├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ └── --feature-request.md └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── SAR.md ├── cicd ├── .DS_Store ├── account_execution │ ├── staging │ │ ├── buildspec.yml │ │ ├── params.json │ │ └── stack.yml │ └── testing │ │ └── buildspec.yml ├── build │ ├── build │ │ ├── buildspec.yml │ │ └── goreleaser.patch │ ├── gitvars │ │ ├── buildspec.yml │ │ └── codebuild-git-wrapper.sh │ └── package │ │ ├── buildspec.yml │ │ ├── release.patch │ │ └── staging.patch ├── cloudformation │ ├── README.md │ ├── developer.yaml │ ├── release.yaml │ ├── secrets.yaml │ └── testing.yaml ├── deploy_patterns │ ├── singlestack │ │ ├── buildspec.yml │ │ ├── namedfunction.yml │ │ └── unnamedfunction.yml │ ├── staging │ │ ├── buildspec.yml │ │ ├── params.json │ │ └── stack.yml │ └── testing │ │ └── buildspec.yml ├── release │ ├── approve │ │ └── buildspec.yml │ └── public │ │ └── buildspec.yml └── tests │ └── account_execution │ ├── cli │ └── buildspec.yml │ └── lambda │ └── buildspec.yml ├── cmd └── root.go ├── go.mod ├── go.sum ├── internal ├── .DS_Store ├── aws │ ├── client.go │ ├── client_test.go │ ├── config.go │ ├── groups.go │ ├── groups_test.go │ ├── http.go │ ├── mock │ │ └── mock_http.go │ ├── schema.go │ ├── users.go │ └── users_test.go ├── config │ ├── config.go │ ├── config_test.go │ └── secrets.go ├── google │ └── client.go ├── mocks │ └── mock_IdentityStoreAPI.go ├── sync.go └── sync_test.go ├── main.go ├── master └── template.yaml /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Ran with args '...' 19 | 3. See error 20 | 21 | 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1Feature request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | 2 | # .github/workflows/main.yaml 3 | name: main 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | - release/* 10 | pull_request: 11 | branches: 12 | - master 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: '1.23.x' 25 | 26 | - name: Install staticcheck 27 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 28 | 29 | - name: Run staticcheck 30 | run: staticcheck ./... 31 | 32 | - name: Install golint 33 | run: go install golang.org/x/lint/golint@latest 34 | 35 | - name: Run golint 36 | run: golint ./... 37 | 38 | - name: Run Tests 39 | run: go test -cover -p 1 -race -v ./... 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yaml 2 | name: release 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: '1.23.x' 20 | 21 | - name: Install staticcheck 22 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 23 | 24 | - name: Run staticcheck 25 | run: staticcheck ./... 26 | 27 | - name: Run Tests 28 | run: go test -p 1 -cover -race -v ./... 29 | 30 | release: 31 | runs-on: ubuntu-latest 32 | needs: [ test ] 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | 37 | - name: Unshallow 38 | run: git fetch --prune --unshallow 39 | 40 | - name: Set up Go 41 | uses: actions/setup-go@v5 42 | with: 43 | go-version: '1.23.x' 44 | 45 | - name: Run GoReleaser 46 | uses: goreleaser/goreleaser-action@v4 47 | with: 48 | version: latest 49 | args: release --clean 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | aws.toml 18 | credentials.json 19 | token.json 20 | dist/ 21 | main 22 | packaged.yaml 23 | 24 | # IDE 25 | .idea/ 26 | .vscode/ 27 | 28 | # SAM 29 | .aws-sam/ 30 | 31 | # Local binary 32 | ssosync 33 | 34 | # Noise from os/editors 35 | .DS_Store 36 | *.swp 37 | */.DS_Store 38 | cicd/.DS_Store 39 | release.yaml 40 | staging.yaml 41 | *.orig 42 | *.rej 43 | cicd/.DS_Store 44 | *.swo 45 | cicd/.DS_Store 46 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # .goreleaser.yml 2 | project_name: ssosync 3 | 4 | before: 5 | hooks: 6 | - go mod download 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - 386 16 | - amd64 17 | - arm 18 | - arm64 19 | ignore: 20 | - goos: darwin 21 | goarch: 386 22 | - goos: windows 23 | goarch: 386 24 | ldflags: 25 | - -s -w -X github.com/awslabs/ssosync/cmd.version={{.Version}} -X github.com/awslabs/ssosync/cmd.commit={{.Commit}} -X github.com/awslabs/ssosync/cmd.date={{.Date}} -X github.com/awslabs/ssosync/cmd.builtBy=goreleaser 26 | checksum: 27 | name_template: '{{ .ProjectName }}_checksums.txt' 28 | changelog: 29 | sort: asc 30 | filters: 31 | exclude: 32 | - '^docs:' 33 | - '^test:' 34 | - Merge pull request 35 | - Merge branch 36 | archives: 37 | - name_template: >- 38 | {{- .ProjectName }}_ 39 | {{- title .Os }}_ 40 | {{- if eq .Arch "amd64" }}x86_64 41 | {{- else if eq .Arch "386" }}i386 42 | {{- else }}{{ .Arch }}{{ end }} 43 | {{- if .Arm }}v{{ .Arm }}{{ end -}} 44 | format_overrides: 45 | - goos: windows 46 | format: zip 47 | 48 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.2 2 | - Fixes panic on IdentityStore user without primary email (common to users created by AWS Control Tower) 3 | - Fixes panic on Google Directory Group with external users with group be synced 4 | 5 | ## 2.0.1 6 | - Fixes m,issing IAM permission identityStore:DeleteGroup 7 | - Updates to developer CICD pipeline 8 | ## 2.0.0 9 | - Introduced the use of the IdentityStore api to overcome various scaling challenge 10 | - Improvements to CICD to allow for testing in different accounts type within an AWS organization 11 | - Strong recommendation to deploy in IAM Identity Center - delegated administration account 12 | 13 | ## 1.1.0 14 | - Added Cloudformation deployable CICD pipelines 15 | - To consistently build and test the application 16 | 17 | ## 1.0.0-rc.10 18 | - #44 fix: ensure old behaviour is supported 19 | - #43 fix: fix ignore-group flag 20 | 21 | ## 1.0.0-rc.9 22 | - #16 feat: additional include-groups option 23 | - #31 improv: limit IAM policy for lambda to access SecretsManager resources 24 | - #18 improv: do not echo sensitive params 25 | - #6 feat: allow group to match regexp 26 | - #36 feat: major refactor, upgrade to Go 1.16, updated dependencies, added capability to sync only groups and selected members 27 | 28 | ## 1.0.0-rc.7 29 | 30 | - #11 Fixing deleted users not synced 31 | 32 | ## 1.0.0-rc.5 33 | 34 | - #7 Groups are synced by their email address 35 | - #9 Disabled users status is synced 36 | 37 | ## 1.0.0-rc.1 38 | 39 | - #1 Fix: Pagination does not work 40 | - #3 Refactor: New features for Serverless Repo and Google best practices 41 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OUTPUT = main # Referenced as Handler in template.yaml 2 | RELEASER = goreleaser 3 | PACKAGED_TEMPLATE = packaged.yaml 4 | STACK_NAME := $(STACK_NAME) 5 | S3_BUCKET := $(S3_BUCKET) 6 | S3_PREFIX := $(S3_PREFIX) 7 | TEMPLATE = template.yaml 8 | APP_NAME ?= ssosync 9 | 10 | 11 | .PHONY: test 12 | test: 13 | go test ./... 14 | 15 | .PHONY: go-build 16 | go-build: 17 | go build -o $(APP_NAME) main.go 18 | 19 | .PHONY: clean 20 | clean: 21 | rm -f $(OUTPUT) $(PACKAGED_TEMPLATE) 22 | 23 | build-SSOSyncFunction: 24 | GOOS=linux GOARCH=arm64 go build -o bootstrap main.go 25 | cp dist/ssosync_linux_arm64/ssosync $(ARTIFACTS_DIR)/bootstrap 26 | 27 | .PHONY: install 28 | install: 29 | go get ./... 30 | 31 | main: main.go 32 | goreleaser build --snapshot --rm-dist 33 | 34 | # compile the code to run in Lambda (local or real) 35 | .PHONY: lambda 36 | lambda: 37 | $(MAKE) main 38 | 39 | .PHONY: build 40 | build: clean lambda 41 | 42 | .PHONY: api 43 | api: build 44 | sam local start-api 45 | 46 | .PHONY: publish 47 | publish: 48 | sam publish -t packaged.yaml 49 | 50 | .PHONY: package 51 | package: build 52 | cp dist/ssosync_linux_arm64/ssosync ./bootstrap 53 | sam package --s3-bucket $(S3_BUCKET) --output-template-file $(PACKAGED_TEMPLATE) --s3-prefix $(S3_PREFIX) 54 | 55 | .PHONY: deploy 56 | deploy: package 57 | sam deploy --stack-name $(STACK_NAME) --template-file $(PACKAGED_TEMPLATE) --capabilities CAPABILITY_IAM 58 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSO Sync 2 | 3 | ![Github Action](https://github.com/awslabs/ssosync/workflows/main/badge.svg) 4 | ![gopherbadger-tag-do-not-edit](https://img.shields.io/badge/Go%20Coverage-42%25-brightgreen.svg?longCache=true&style=flat) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/awslabs/ssosync)](https://goreportcard.com/report/github.com/awslabs/ssosync) 6 | [![License Apache 2](https://img.shields.io/badge/License-Apache2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) 7 | [![Taylor Swift](https://img.shields.io/badge/secured%20by-taylor%20swift-brightgreen.svg)](https://twitter.com/SwiftOnSecurity) 8 | 9 | ## Quick Start 10 | Want to dive straight in get ssisync up and running? Then this [lab](https://catalog.workshops.aws/control-tower/en-US/authentication-authorization/google-workspace) in the [AWS Control Tower Workshop](https://catalog.workshops.aws/control-tower/en-US) (you don't need AWS Control Tower, this lab only uses IAM Identity Center which is typically deployed by AWS Control Tower). The lab will guide you through the full setup process on both AWS and Google Workspace, using the Lambda from the Serverless application repository, which is the recommend and simplist deployment method. 11 | 12 | > Helping you populate AWS SSO directly with your Google Apps users 13 | 14 | SSO Sync will run on any platform that Go can build for. It is available in the [AWS Serverless Application Repository](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync) 15 | 16 | > [!CAUTION] 17 | > When using ssosync with an instance of IAM Identity Center integrated with AWS Control Tower. AWS Control Tower creates a number of groups and users (directly via the Identity Store API), when an external identity provider is configured these users and groups are can not be used to log in. However it is important to remember that because ssosync implemements a uni-directional sync it will make the IAM Identity Store match the subset of your Google Workspaces directory you specify, including removing these groups and users created by AWS Control Tower. There is a PFR [#179 Configurable handling of 'manually created' Users/Groups in IAM Identity Center](https://github.com/awslabs/ssosync/issues/179) to implement an option to ignore these users and groups, hopefully this will be implemented in version 3.x. However, this has a dependancy on PFR [#166 Ensure all groups/user creates in IAM Identity Store are via SCIM api and populate externalId field](https://github.com/awslabs/ssosync/issues/166), to be able to reliably and consistently disinguish between **SCIM Provisioned** users from **Manually Created** users 18 | 19 | > [!WARNING] 20 | > There are breaking changes for versions `>= 0.02` 21 | 22 | > [!WARNING] 23 | > `>= 1.0.0-rc.5` groups to do not get deleted in AWS SSO when deleted in the Google Directory, and groups are synced by their email address 24 | 25 | > [!WARNING] 26 | > `>= 2.0.0` this makes use of the **Identity Store API** which means: 27 | > * if deploying the lambda from the [AWS Serverless Application Repository](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync) then it needs to be deployed into the [IAM Identity Center delegated administration](https://docs.aws.amazon.com/singlesignon/latest/userguide/delegated-admin.html) account. Technically you could deploy in the management account but we would recommend against this. 28 | > * if you are running the project as a cli tool, then the environment will need to be using credentials of a user in the [IAM Identity Center delegated administration](https://docs.aws.amazon.com/singlesignon/latest/userguide/delegated-admin.html) account, with appropriate permissions. 29 | 30 | > [!WARNING] 31 | > `>= 2.1.0` make use of named IAM resources, so if deploying via CICD or IaC template will require **CAPABILITY_NAMED_IAM** to be specified. 32 | 33 | > [!IMPORTANT] 34 | > `>= 2.1.0` switched to using `provided.al2` powered by ARM64 instances. 35 | 36 | > [!IMPORTANT] 37 | > As of `v2.2.0` multiple query patterns are supported for both Group and User matching, simply separate each query with a `,`. For full sync of groups and/or users specify '*' in the relevant match field. 38 | > User match and group match can now be used in combination with the sync method of groups. 39 | > Nested groups will now be flattened into the top level groups. 40 | > External users are ignored. 41 | > Group owners are treated as regular group members. 42 | > User details are now cached to reduce the number of api calls and improve execution times on large directories. 43 | 44 | ## Why? 45 | 46 | As per the [AWS SSO](https://aws.amazon.com/single-sign-on/) Homepage: 47 | 48 | > AWS Single Sign-On (SSO) makes it easy to centrally manage access 49 | > to multiple AWS accounts and business applications and provide users 50 | > with single sign-on access to all their assigned accounts and applications 51 | > from one place. 52 | 53 | Key part further down: 54 | 55 | > With AWS SSO, you can create and manage user identities in AWS SSO’s 56 | >identity store, or easily connect to your existing identity source including 57 | > Microsoft Active Directory and **Azure Active Directory (Azure AD)**. 58 | 59 | AWS SSO can use other Identity Providers as well... such as Google Apps for Domains. Although AWS SSO 60 | supports a subset of the SCIM protocol for populating users, it currently only has support for Azure AD. 61 | 62 | This project provides a CLI tool to pull users and groups from Google and push them into AWS SSO. 63 | `ssosync` deals with removing users as well. The heavily commented code provides you with the detail of 64 | what it is going to do. 65 | 66 | ### References 67 | 68 | * [SCIM Protocol RFC](https://tools.ietf.org/html/rfc7644) 69 | * [AWS SSO - Connect to Your External Identity Provider](https://docs.aws.amazon.com/singlesignon/latest/userguide/manage-your-identity-source-idp.html) 70 | * [AWS SSO - Automatic Provisioning](https://docs.aws.amazon.com/singlesignon/latest/userguide/provision-automatically.html) 71 | * [AWS IAM Identity Center - Identity Store API](https://docs.aws.amazon.com/singlesignon/latest/IdentityStoreAPIReference/welcome.html) 72 | 73 | ## Installation 74 | 75 | The recommended installation is: 76 | * [Setup IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/get-started-enable-identity-center.html), in the management account of your organization 77 | * Created a linked account `Identity` Account from which to manage IAM Identity Center 78 | * [Delegate administration](https://docs.aws.amazon.com/singlesignon/latest/userguide/delegated-admin.html) to the `Identity` account 79 | * Deploy the [SSOSync app](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync) from the AWS Serverless Application Repository 80 | 81 | 82 | You can also: 83 | You can `go get github.com/awslabs/ssosync` or grab a Release binary from the release page. The binary 84 | can be used from your local computer, or you can deploy to AWS Lambda to run on a CloudWatch Event 85 | for regular synchronization. 86 | 87 | ## Configuration 88 | 89 | You need a few items of configuration. One side from AWS, and the other 90 | from Google Cloud to allow for API access to each. You should have configured 91 | Google as your Identity Provider for AWS SSO already. 92 | 93 | You will need the files produced by these steps for AWS Lambda deployment as well 94 | as locally running the ssosync tool. 95 | 96 | ### Google 97 | 98 | First, you have to setup your API. In the project you want to use go to the [Console](https://console.developers.google.com/apis) and select *API & Services* > *Enable APIs and Services*. Search for *Admin SDK* and *Enable* the API. 99 | 100 | You have to perform this [tutorial](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) to create a service account that you use to sync your users. Save the `JSON file` you create during the process and rename it to `credentials.json`. 101 | 102 | > you can also use the `--google-credentials` parameter to explicitly specify the file with the service credentials. Please, keep this file safe, or store it in the AWS Secrets Manager 103 | 104 | In the domain-wide delegation for the Admin API, you have to specify the following scopes for the user. 105 | 106 | * https://www.googleapis.com/auth/admin.directory.group.readonly 107 | * https://www.googleapis.com/auth/admin.directory.group.member.readonly 108 | * https://www.googleapis.com/auth/admin.directory.user.readonly 109 | 110 | Back in the Console go to the Dashboard for the API & Services and select "Enable API and Services". 111 | In the Search box type `Admin` and select the `Admin SDK` option. Click the `Enable` button. 112 | 113 | You will have to specify the email address of an admin via `--google-admin` to assume this users role in the Directory. 114 | 115 | ### AWS 116 | 117 | Go to the AWS Single Sign-On console in the region you have set up AWS SSO and select 118 | Settings. Click `Enable automatic provisioning`. 119 | 120 | A pop up will appear with URL and the Access Token. The Access Token will only appear 121 | at this stage. You want to copy both of these as a parameter to the `ssosync` command. 122 | 123 | Or you specific these as environment variables. 124 | 125 | ```bash 126 | SSOSYNC_SCIM_ACCESS_TOKEN= 127 | SSOSYNC_SCIM_ENDPOINT= 128 | ``` 129 | 130 | Additionally, authenticate your AWS credentials. Follow this [section](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#:~:text=Creating%20the%20Credentials%20File) to create a Shared Credentials File in the home directory or export your Credentials with Environment Variables. Ensure that the default credentials are for the AWS account you intended to be synced. 131 | 132 | To obtain your `Identity store ID`, go to the AWS Identity Center console and select settings. Under the `Identity Source` section, copy the `Identity store ID`. 133 | 134 | ## Local Usage 135 | 136 | ```bash 137 | git clone https://github.com/awslabs/ssosync.git 138 | cd ssosync/ 139 | make go-build 140 | ``` 141 | 142 | ```bash 143 | ./ssosync --help 144 | ``` 145 | 146 | ```bash 147 | A command line tool to enable you to synchronise your Google 148 | Apps (Google Workspace) users to AWS Single Sign-on (AWS SSO) 149 | Complete documentation is available at https://github.com/awslabs/ssosync 150 | 151 | Usage: 152 | ssosync [flags] 153 | 154 | Flags: 155 | -t, --access-token string AWS SSO SCIM API Access Token 156 | -d, --debug enable verbose / debug logging 157 | -e, --endpoint string AWS SSO SCIM API Endpoint 158 | -u, --google-admin string Google Workspace admin user email 159 | -c, --google-credentials string path to Google Workspace credentials file (default "credentials.json") 160 | -g, --group-match string Google Workspace Groups filter query parameter, a simple '*' denotes sync all groups (and any users that are members of those groups). example: 'name:Admin*,email:aws-*', 'name=Admins' or '*' see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups, if left empty no groups will be selected. 161 | -h, --help help for ssosync 162 | --ignore-groups strings ignores these Google Workspace groups 163 | --ignore-users strings ignores these Google Workspace users 164 | --include-groups strings include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups' 165 | --log-format string log format (default "text") 166 | --log-level string log level (default "info") 167 | -s, --sync-method string Sync method to use (users_groups|groups) (default "groups") 168 | -m, --user-match string Google Workspace Users filter query parameter, a simple '*' denotes sync all users in the directory. example: 'name:John*,email:admin*', '*' or name=John Doe,email:admin*' see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users, if left empty no users will be selected but if a pattern has been set for GroupMatch users that are members of the groups it matches will still be selected 169 | -v, --version version for ssosync 170 | -r, --region AWS region where identity store exists 171 | -i, --identity-store-id AWS Identity Store ID 172 | ``` 173 | 174 | The function has `two behaviour` and these are controlled by the `--sync-method` flag, this behavior could be 175 | 176 | 1. `groups`: __(default)__ The sync procedure work base on Groups, gets the Google Workspace groups and their members, then creates in AWS SSO the users (members of the Google Workspace groups), then the groups and at the end assign the users to their respective groups. 177 | 2. `users_groups`: __(original behavior, previous versions)__ The sync procedure is simple, gets the Google Workspace users and creates these in AWS SSO Users; then gets Google Workspace groups and creates these in AWS SSO Groups and assigns users to belong to the AWS SSO Groups. 178 | 179 | Flags Notes: 180 | 181 | * `--include-groups` only works when `--sync-method` is `users_groups` 182 | * `--ignore-users` works for both `--sync-method` values. Example: `--ignore-users user1@example.com,user2@example.com` or `SSOSYNC_IGNORE_USERS=user1@example.com,user2@example.com` 183 | * `--ignore-groups` works for both `--sync-method` values. Example: --ignore-groups group1@example.com,group1@example.com` or `SSOSYNC_IGNORE_GROUPS=group1@example.com,group1@example.com` 184 | * `--group-match` works for both `--sync-method` values and also in combination with `--ignore-groups` and `--ignore-users`. This is the filter query passed to the [Google Workspace Directory API when search Groups](https://developers.google.com/admin-sdk/directory/v1/guides/search-groups), if the flag is not used, groups are not filtered. 185 | * `--user-match` works for both `--sync-method` values and also in combination with `--ignore-groups` and `--ignore-users`. This is the filter query passed to the [Google Workspace Directory API when search Users](https://developers.google.com/admin-sdk/directory/v1/guides/search-users), if the flag is not used, users are not filtered. 186 | 187 | > [!NOTE] 188 | > 1. Depending on the number of users and groups you have, maybe you can get `AWS SSO SCIM API rate limits errors`, and more frequently happens if you execute the sync many times in a short time. 189 | > 2. Depending on the number of users and groups you have, `--debug` flag generate too much logs lines in your AWS Lambda function. So test it in locally with the `--debug` flag enabled and disable it when you use a AWS Lambda function. 190 | 191 | ## AWS Lambda Usage 192 | 193 | > [!TIP] 194 | > Using Lambda may incur costs in your AWS account. Please make sure you have checked 195 | the pricing for AWS Lambda and CloudWatch before continuing. 196 | 197 | Additionally, before choosing to deploy with Lambda, please ensure that the [AWS Lambda SLAs](https://aws.amazon.com/lambda/sla/) are sufficient for your use cases. 198 | 199 | Running ssosync once means that any changes to your Google directory will not appear in 200 | AWS SSO. To sync regularly, you can run ssosync via AWS Lambda. 201 | 202 | > [!WARNING] 203 | > You find it in the [AWS Serverless Application Repository](https://eu-west-1.console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync). 204 | 205 | > [!TIP] 206 | > ### v2.1 Changes 207 | > * user and group selection fields in the Cloudformation template can now be left empty where not required and will not be added as environment variables to the Lambda function, this provides consistency with CLI use of ssosync. 208 | > * Stronger validation of parameters in the Cloudformation template, to improve likelhood of success for new users. 209 | > * Now supports multiple deployment patterns, defaults are consistent with previous versions. 210 | 211 | **App + secrets** This is the default mode and fully backwards compatible with previous versions 212 | 213 | **App only** This mode does not create the secrets but expects you to deployed a separate stack using the **Secrets only** mode within the same account 214 | > [!CAUTION] 215 | > If you want to use your own existing secrets then provide them as a comma separated list in the ##CrossStackConfigI## field in the following order: 216 | > __GoogleCredentials ARN__,__GoogleAdminEmail ARN__,__SCIMEndpoint ARN__,__SCIMAccessToken ARN__,__Region ARN__,__IdentityStoreID ARN__ 217 | > 218 | **App for cross-account** This mode is used where you have deployed the secrets in a separate account, the arns of the KMS key and secrets need to be passed into the __CrossStackConfig__ field, It is easiest to have created the secrets in the other account using the ** Secrest for cross-account** mode, as the output can simply copied and pasted into the above field. 219 | 220 | > [!CAUTION] 221 | > If you want to use your own existing secrets then provide them as a comma separated list in the __CrossStackConfig__ field in the following order: 222 | > __GoogleCredentials ARN__,__GoogleAdminEmail ARN__,__SCIMEndpoint ARN__,__SCIMAccessToken ARN__,__Region ARN__,__IdentityStoreID ARN__,__KMS Key ARN__ 223 | 224 | > [!IMPORTANT] 225 | > Be sure to allow access to the key and secrets in their respective policies to the role __SSOSyncAppRole__ in the app account. 226 | 227 | **Secrets only** This mode creates a set of secrets but does not deploy the app itself, it requires the app is deployed in that same account using the **App only** mode. This allows for decoupling of the secrets and the app. 228 | 229 | **Secrets for cross-account** This mode creates a set of secrets and KMS key but does not deploy the app itself, this is for use with an app stack, deployed using the **App for cross-account** mode. This allows for a single set of secrets to be shared with multipl app instance for testing, and improve secrets security. 230 | 231 | ## SAM 232 | 233 | You can use the AWS Serverless Application Model (SAM) to deploy this to your account. 234 | 235 | > Please, install the [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) and [GoReleaser](https://goreleaser.com/install/). 236 | 237 | Specify an Amazon S3 Bucket for the upload with `export S3_BUCKET=` and an S3 prefix with `export S3_PREFIX=`. 238 | 239 | Execute `make package` in the console. Which will package and upload the function to the bucket. You can then use the `packaged.yaml` to configure and deploy the stack in [AWS CloudFormation Console](https://console.aws.amazon.com/cloudformation). 240 | 241 | ### Example 242 | 243 | Build 244 | 245 | ```bash 246 | aws cloudformation validate-template --template-body file://template.yaml 1>/dev/null && 247 | sam validate && 248 | sam build 249 | ``` 250 | 251 | Deploy 252 | 253 | ```bash 254 | sam deploy --guided 255 | ``` 256 | 257 | ## License 258 | 259 | [Apache-2.0](/LICENSE) 260 | -------------------------------------------------------------------------------- /SAR.md: -------------------------------------------------------------------------------- 1 | # SSO Sync 2 | 3 | This AWS Serverless Application populates AWS SSO directly with your Google Apps users. 4 | 5 | ## Setup 6 | 7 | Before you start, you have to [enable AWS SSO](https://docs.aws.amazon.com/singlesignon/latest/userguide/step1.html) in AWS Organizations. The next steps are to configure the access to the Google APIs and the AWS SSO SCIM endpoint. 8 | 9 | ### Google 10 | 11 | First, you have to setup your API. In the project you want to use go to the [Console](https://console.developers.google.com/apis) and select *API & Services* > *Enable APIs and Services*. Search for *Admin SDK* and *Enable* the API. 12 | 13 | You have to perform this [tutorial](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) to create a service account that you use to sync your users. Save the JSON file you create during the process and rename it to `credentials.json`. 14 | 15 | In the domain-wide delegation for the Admin API, you have to specificy the following scopes for the user. 16 | 17 | `https://www.googleapis.com/auth/admin.directory.group.readonly,https://www.googleapis.com/auth/admin.directory.group.member.readonly,https://www.googleapis.com/auth/admin.directory.user.readonly` 18 | 19 | Back in the Console go to the Dashboard for the API & Services and select "Enable API and Services". 20 | In the Search box type `Admin` and select the `Admin SDK` option. Click the `Enable` button. 21 | 22 | There are general configuration parameters to the application stack. 23 | 24 | * `GoogleCredentials` contains the content of the `credentials.json` file 25 | * `GoogleAdminEmail` contains the email address of an admin 26 | 27 | The secrets are stored in the [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/). 28 | 29 | ### AWS 30 | 31 | Go to the AWS Single Sign-On console in the region you have set up AWS SSO and select 32 | Settings. Click `Enable automatic provisioning`. 33 | 34 | A pop up will appear with URL and the Access Token. The Access Token will only appear 35 | at this stage. You want to copy both of these to the stack parameters. 36 | 37 | * `SCIMEndpointUrl` 38 | * `SCIMEndpointAccessToken` 39 | 40 | You are ready to either to deploy the application to your account. 41 | -------------------------------------------------------------------------------- /cicd/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/ssosync/beff54f81747f1b1c31553f161d885b00683b3d5/cicd/.DS_Store -------------------------------------------------------------------------------- /cicd/account_execution/staging/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | 5 | build: 6 | commands: 7 | # Create parameters 8 | - export AppVersion="${GitTag#v}-${GitVersionHash}" 9 | 10 | # Copy in the executable 11 | - cp ${CODEBUILD_SRC_DIR_Built}/dist/ssosync_linux_amd64_v1/ssosync ./ 12 | 13 | # Copy in the tests 14 | - cp -r cicd/tests ./ 15 | 16 | # Copy in the stack and params templates 17 | - mkdir deploy 18 | - cp cicd/account_execution/staging/stack.yml ./deploy/ 19 | 20 | # Update params with the values for this run for a developer account 21 | - | 22 | jq -n \ 23 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:AWS*,name=NestedGroups\"}" \ 24 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 25 | '$ARGS.named' > ./deploy/developer.json 26 | - cat ./deploy/developer.json 27 | 28 | # Update params with the values for this run for the management account 29 | - | 30 | jq -n \ 31 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:Man*\"}" \ 32 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 33 | '$ARGS.named' > ./deploy/cli.json 34 | - cat ./deploy/cli.json 35 | 36 | # Update params with the values for this run for the delegated account 37 | - | 38 | jq -n \ 39 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:Del*\"}" \ 40 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 41 | '$ARGS.named' > ./deploy/lambda.json 42 | - cat ./deploy/lambda.json 43 | 44 | # Update params with the values for this run for non-delegated account 45 | - | 46 | jq -n \ 47 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:Non*\"}" \ 48 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 49 | '$ARGS.named' > ./deploy/codepipeline.json 50 | - cat ./deploy/codepipeline.json 51 | 52 | 53 | artifacts: 54 | files: 55 | - ssosync 56 | - deploy/**/* 57 | - tests/**/* 58 | -------------------------------------------------------------------------------- /cicd/account_execution/staging/params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "AppArn": "APPARN", 4 | "AppVersion": "APPVERSION" 5 | }, 6 | "StackPolicy": { 7 | "Statement": [{ 8 | "Effect": "Allow", 9 | "NotAction": "Update:Delete", 10 | "Principal": "*", 11 | "Resource": "*" 12 | }] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cicd/account_execution/staging/stack.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Description: 5 | This CloudFormation template will deploy an instance of the SSOSync-Staging 6 | candidate releases (via privately shared app in the AWS Serverless Application 7 | Repository (SAR) within the Staging Account. 8 | 9 | Parameters: 10 | AppArn: 11 | Description: The candidate release in the SAR 12 | Default: 'arn:aws:serverlessrepo:::applications/' 13 | Type: String 14 | AppVersion: 15 | Description: The version of this build in SAR 16 | Default: 'v1.0.0-rc.10' 17 | Type: String 18 | GoogleAdminEmailArn: 19 | Type: String 20 | GoogleCredentialsArn: 21 | Type: String 22 | SCIMEndpointUrlArn: 23 | Type: String 24 | SCIMAccessTokenArn: 25 | Type: String 26 | RegionArn: 27 | Type: String 28 | IdentityStoreIdArn: 29 | Type: String 30 | GroupMatch: 31 | Description: The search string to match Groups in Google Workspace 32 | Default: 'name:AWS*' 33 | Type: String 34 | 35 | Resources: 36 | SARApp: 37 | Type: AWS::Serverless::Application 38 | Properties: 39 | Location: 40 | ApplicationId: !Ref AppArn 41 | SemanticVersion: !Ref AppVersion 42 | Parameters: 43 | FunctionName: SSOSyncFunction 44 | GoogleAdminEmail: !Join 45 | - '' 46 | - - '{{resolve:secretsmanager:' 47 | - !Ref GoogleAdminEmailArn 48 | - '}}' 49 | GoogleCredentials: !Join 50 | - '' 51 | - - '{{resolve:secretsmanager:' 52 | - !Ref GoogleCredentialsArn 53 | - '}}' 54 | SCIMEndpointUrl: !Join 55 | - '' 56 | - - '{{resolve:secretsmanager:' 57 | - !Ref SCIMEndpointUrlArn 58 | - '}}' 59 | SCIMEndpointAccessToken: !Join 60 | - '' 61 | - - '{{resolve:secretsmanager:' 62 | - !Ref SCIMAccessTokenArn 63 | - '}}' 64 | Region: !Join 65 | - '' 66 | - - '{{resolve:secretsmanager:' 67 | - !Ref RegionArn 68 | - '}}' 69 | IdentityStoreID: !Join 70 | - '' 71 | - - '{{resolve:secretsmanager:' 72 | - !Ref IdentityStoreIdArn 73 | - '}}' 74 | SyncMethod: groups 75 | GoogleGroupMatch: !Ref GroupMatch 76 | LogLevel: info 77 | LogFormat: json 78 | -------------------------------------------------------------------------------- /cicd/account_execution/testing/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | ShareWith: "NOT-SHARED" 6 | interval: 10 7 | Success: '"Succeeded"' 8 | InProgress: '"InProgress"' 9 | Status: '"InProgress"' 10 | 11 | phases: 12 | pre_build: 13 | commands: 14 | # Print all environment variables (handy for AWS CodeBuild logs 15 | - env 16 | 17 | build: 18 | commands: 19 | # zip up the content of TESTS 20 | - cp -r ${CODEBUILD_SRC_DIR_Tests}/* ./ 21 | - zip -r tests.zip ./ssosync 22 | - zip -r tests.zip ./tests 23 | - zip -r tests.zip ./deploy 24 | 25 | # Auth into the Staging Account 26 | - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role --role-arn "${StagingRole}" --role-session-name "CodePipelineRole" --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text)) 27 | 28 | # upload the zipfile to the S3 Bucket 29 | - aws s3 cp ./tests.zip s3://${TARGETS3BUCKET}/ 30 | 31 | # Start the test pipeline in the staging account 32 | - export ExecutionId=$(aws codepipeline start-pipeline-execution --name $pipeline --output text) 33 | - echo "ExecutionId=" $ExecutionId 34 | 35 | - | 36 | while expr "$Status" : "$InProgress" >/dev/null; do 37 | sleep $interval 38 | export Status="$(aws codepipeline get-pipeline-execution --pipeline-name $pipeline --output json --pipeline-execution-id $ExecutionId --query "pipelineExecution.status")" 39 | echo $Status 40 | done 41 | 42 | - echo "We are done" 43 | 44 | - | 45 | if expr "$Status" : "$Success" >/dev/null; then 46 | exit 0 47 | else 48 | exit 252 49 | fi 50 | 51 | -------------------------------------------------------------------------------- /cicd/build/build/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | GO111MODULE: "on" 6 | git-credential-helper: yes 7 | 8 | phases: 9 | install: 10 | commands: 11 | # Add goreleaser repo 12 | - echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list 13 | 14 | # Update the repos 15 | - apt -qq --yes update 16 | - apt -qq --yes upgrade 17 | 18 | # Install go.lang 19 | - GoVersion=${GOLANG_20_VERSION} 20 | 21 | # Install golint - now deprecated 22 | - go install golang.org/x/lint/golint@latest 23 | 24 | # Install staticcheck - use static install from tarball 25 | - wget -qO- https://github.com/dominikh/go-tools/releases/download/2023.1.6/staticcheck_linux_386.tar.gz | tar -xvz -C ./ 26 | 27 | # Install Testify to use common assertions and mocks in tests 28 | - go get github.com/stretchr/testify 29 | 30 | # Install goreleaser - go install method broken due to dependancies using apt static binary approach 31 | # - go install github.com/goreleaser/goreleaser@latest 32 | - apt -qq --yes install goreleaser 33 | 34 | pre_build: 35 | commands: 36 | # Print all environment variables (handy for AWS CodeBuild logs) 37 | - env 38 | 39 | # Fetch all dependencies 40 | # - go env -w GOPROXY=direct 41 | - go get ./... 42 | 43 | # Run staticcheck 44 | - staticcheck/staticcheck ./... 45 | 46 | # Ensure code passes all lint tests 47 | #- golint -set_exit_status ./... 48 | 49 | # Check the Go code for common problems with 'go vet' 50 | - go vet ./... 51 | 52 | # Run all tests included with our application 53 | - go test ./... 54 | 55 | build: 56 | commands: 57 | # Make clean 58 | - rm -f main packaged.yaml 59 | 60 | # Make go-build 61 | - go build -o ${APP_NAME} main.go 62 | 63 | # Does it exist? 64 | - ls ${APP_NAME} 65 | 66 | # Does it run? 67 | - ./${APP_NAME} --version 68 | 69 | post_build: 70 | commands: 71 | # Tweak the .goreleaser.yml so it uses the vairables from .Env 72 | - patch .goreleaser.yml cicd/build/build/goreleaser.patch 73 | 74 | # Make main 75 | - goreleaser build --snapshot --clean 76 | 77 | 78 | # Check we've packaged something useful 79 | - ./dist/ssosync_linux_amd64_v1/ssosync --version 80 | 81 | artifacts: 82 | files: 83 | - ${APP_NAME} 84 | - dist/**/* 85 | -------------------------------------------------------------------------------- /cicd/build/build/goreleaser.patch: -------------------------------------------------------------------------------- 1 | --- .goreleaser.yml.default 2023-10-25 11:30:58 2 | +++ .goreleaser.yml 2023-10-25 11:32:18 3 | @@ -9,20 +9,11 @@ 4 | - CGO_ENABLED=0 5 | goos: 6 | - linux 7 | - - darwin 8 | - - windows 9 | goarch: 10 | - - 386 11 | - amd64 12 | - - arm 13 | - arm64 14 | - ignore: 15 | - - goos: darwin 16 | - goarch: 386 17 | - - goos: windows 18 | - goarch: 386 19 | ldflags: 20 | - - -s -w -X github.com/awslabs/ssosync/cmd.version={{.Version}} -X github.com/awslabs/ssosync/cmd.commit={{.Commit}} -X github.com/awslabs/ssosync/cmd.date={{.Date}} -X github.com/awslabs/ssosync/cmd.builtBy=goreleaser 21 | + - -s -w -X github.com/awslabs/ssosync/cmd.version={{.Env.GitTag}} -X github.com/awslabs/ssosync/cmd.commit={{.Env.GitCommit}} -X github.com/awslabs/ssosync/cmd.date={{.Date}} -X github.com/awslabs/ssosync/cmd.builtBy=goreleaser -X github.com/awslabs/ssosync/cmd.goversion={{.Env.GoVersion}} 22 | checksum: 23 | name_template: '{{ .ProjectName }}_checksums.txt' 24 | changelog: 25 | -------------------------------------------------------------------------------- /cicd/build/gitvars/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | exported-variables: 5 | - Project 6 | - Branch 7 | - Tag 8 | - CommitId 9 | - CommitMessage 10 | - CommitAuthor 11 | - CommitAuthorEmail 12 | - CommitHash 13 | - IsRelease 14 | 15 | phases: 16 | pre_build: 17 | commands: 18 | # Fetch all dependencies 19 | - chmod +x cicd/build/gitvars/codebuild-git-wrapper.sh 20 | - cicd/build/gitvars/codebuild-git-wrapper.sh $GitRepo $GitBranch 21 | 22 | build: 23 | commands: 24 | - Branch=`git branch -a --contains HEAD | sed -n 2p | awk '{ printf $1 }'` 25 | - export Branch="${Branch#remotes/origin/}" 26 | - export Project="${APP_Name}" 27 | - export Tag=`git describe --tags --abbrev=0` 28 | - export CommitId="$(git log -1 --pretty=%H)" 29 | - export CommitMessage="$(git log -1 --pretty=%B)" 30 | - export CommitAuthor="$(git log -1 --pretty=%an)" 31 | - export CommitAuthorEmail="$(git log -1 --pretty=%ae)" 32 | - export CommitHash=`git rev-parse --short HEAD` 33 | 34 | - GitReleaseCommit=`git rev-list -n 1 tags/${Tag}` 35 | - echo GitReleaseCommit=$GitReleaseCommit 36 | - echo CommitId=$CommitId 37 | 38 | - | 39 | if expr "${GitReleaseCommit}" : "${CommitId}" >/dev/null; then 40 | IsRelease=true 41 | else 42 | IsRelease=false 43 | fi 44 | 45 | - echo $IsRelease 46 | 47 | -------------------------------------------------------------------------------- /cicd/build/gitvars/codebuild-git-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # This is a workaround for a limitation of CodeBuild / CodePipeline, where the git metadata is missing. 4 | # It brings in the git metadata by checking out the repository and winding it to the currently building commit. 5 | # See https://itnext.io/how-to-access-git-metadata-in-codebuild-when-using-codepipeline-codecommit-ceacf2c5c1dc? 6 | # for the rationale and description. 7 | 8 | # (C) Timothy Jones, https://github.com/TimothyJones/codepipeline-git-metadata-example 9 | # MIT License, see https://github.com/TimothyJones/codepipeline-git-metadata-example/blob/master/LICENSE 10 | 11 | # This function prints the usage 12 | function usage { 13 | { 14 | echo "Usage:" 15 | echo " ${BASH_SOURCE[0]} " 16 | echo " REPO_URL - the URL for the CodeCommit repository" 17 | echo " BRANCH - (optional) the branch to check out. Defaults to the default branch." 18 | } >&2 19 | } 20 | 21 | # Confirm that there are at least two arguments 22 | if [ "$#" -lt 1 ]; then 23 | usage 24 | exit 1 25 | fi 26 | 27 | # Confirm that CODEBUILD_RESOLVED_SOURCE_VERSION is set 28 | if [ -z "${CODEBUILD_RESOLVED_SOURCE_VERSION:-}" ]; then 29 | { 30 | echo "Error: CODEBUILD_RESOLVED_SOURCE_VERSION is not set" 31 | } >&2 32 | usage 33 | exit 1 34 | fi 35 | 36 | # Read arguments 37 | REPO_URL="$1" 38 | if [ ! -z "${2:-}" ]; then 39 | BRANCH=$2 40 | fi 41 | 42 | # Remember the working directory 43 | WORKING_DIR="$(pwd)" 44 | 45 | # Check out the repository to a temporary directory 46 | # Note that --quiet doesn't work on the current CodeBuild agents, but 47 | # hopefully it will in the future 48 | TEMP_FOLDER="$(mktemp -d)" 49 | git clone --quiet "$REPO_URL" "$TEMP_FOLDER" 50 | 51 | # Wind the repository back to the specified branch and commit 52 | cd "$TEMP_FOLDER" 53 | git fetch --tags 54 | if [ ! -z "${BRANCH:-}" ]; then 55 | git checkout "$BRANCH" 56 | fi 57 | git reset --hard "$CODEBUILD_RESOLVED_SOURCE_VERSION" 58 | 59 | # Confirm that the git checkout worked 60 | if [ ! -d .git ] ; then 61 | { 62 | echo "Error: .git directory missing. Git checkout probably failed" 63 | } >&2 64 | exit 1 65 | fi 66 | 67 | mv .git "$WORKING_DIR" 68 | -------------------------------------------------------------------------------- /cicd/build/package/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | ShareWith: "NOT-SHARED" 6 | 7 | phases: 8 | pre_build: 9 | commands: 10 | # Print all environment variables (handy for AWS CodeBuild logs) 11 | - env 12 | 13 | - cp -r ${CODEBUILD_SRC_DIR_Built}/* ./ 14 | - ls -la 15 | 16 | # Check that the files need to package exist 17 | - ls README.md 18 | - ls SAR.md 19 | - ls dist/ssosync_linux_arm64_v8.0/ssosync 20 | - ls dist/ssosync_linux_amd64_v1/ssosync 21 | 22 | # Check that the executable works 23 | - ./dist/ssosync_linux_amd64_v1/ssosync --version 24 | - mv dist/ssosync_linux_arm64_v8.0/ssosync bootstrap 25 | 26 | build: 27 | commands: 28 | # Create staging & release variants of the template.yaml 29 | - cp template.yaml staging.yaml 30 | - patch staging.yaml cicd/build/package/staging.patch 31 | - sam package --no-progressbar --template-file staging.yaml --s3-bucket ${S3Bucket} --output-template-file packaged-staging.yaml 32 | 33 | - cp template.yaml release.yaml 34 | - patch release.yaml cicd/build/package/release.patch 35 | - sam package --no-progressbar --template-file release.yaml --s3-bucket ${S3Bucket} --output-template-file packaged-release.yaml 36 | 37 | - ls packaged-staging.yaml 38 | - ls packaged-release.yaml 39 | 40 | post_build: 41 | commands: 42 | # Create parameters 43 | - export AppVersion="${GitTag#v}-${GitVersionHash}" 44 | - aws ssm put-parameter --name "/SSOSync/Staging/Version" --value ${AppVersion} --type String --overwrite 45 | 46 | # remove the previous builds 47 | #- aws serverlessrepo delete-application --application-id ${AppArn} 48 | 49 | # Package our application with AWS SAM 50 | - echo sam publish --template packaged-staging.yaml --semantic-version ${AppVersion} 51 | - sam publish --template packaged-staging.yaml --semantic-version ${AppVersion} 52 | 53 | # Share with the StagingAccount 54 | - | 55 | if expr "${ShareWith}" : "NOT-SHARED" >/dev/null; then 56 | echo "Skipping Sharing" 57 | else 58 | aws serverlessrepo put-application-policy --application-id ${AppArn} --statements Principals=${ShareWith},Actions=Deploy 59 | fi 60 | 61 | artifacts: 62 | files: 63 | - packaged-staging.yaml 64 | - packaged-release.yaml 65 | -------------------------------------------------------------------------------- /cicd/build/package/release.patch: -------------------------------------------------------------------------------- 1 | --- template.yaml 2023-10-27 16:34:16 2 | +++ release.yaml 2023-10-27 16:34:37 3 | @@ -36,7 +36,7 @@ 4 | - ScheduleExpression 5 | 6 | AWS::ServerlessRepo::Application: 7 | - Name: ssosync 8 | + Name: SSOSync 9 | Description: Helping you populate AWS SSO directly with your Google Apps users. 10 | Author: Sebastian Doell 11 | SpdxLicenseId: Apache-2.0 12 | -------------------------------------------------------------------------------- /cicd/build/package/staging.patch: -------------------------------------------------------------------------------- 1 | --- template.yaml 2023-10-30 14:21:20 2 | +++ staging.yaml 2023-10-30 14:21:59 3 | @@ -38,7 +38,7 @@ 4 | - ScheduleExpression 5 | 6 | AWS::ServerlessRepo::Application: 7 | - Name: ssosync 8 | + Name: SSOSync-Staging 9 | Description: Helping you populate AWS SSO directly with your Google Apps users. 10 | Author: Sebastian Doell 11 | SpdxLicenseId: Apache-2.0 12 | -------------------------------------------------------------------------------- /cicd/cloudformation/README.md: -------------------------------------------------------------------------------- 1 | # SSO Sync Pipelines 2 | 3 | There are a number of cloudformation templates depending on what your need to deploy. For most developers 4 | You need 5 | * secrets.yaml - creates the secrets for storing the credentials for your test GSuite and IAM Identity Center instances 6 | * developer.yaml - creates the pipeline to build and test prior to raising a pull request. 7 | 8 | The other option is for the production build, deploy and test environment, which requires, two AWS accounts *production* and *staging*: 9 | * Create the management account 10 | * setup organizations 11 | * create two linked accounts (delegated & non-delegated respectively) 12 | * setup IAM Identity Center 13 | * delegate administration to the *delegated* account 14 | 15 | * Deploy the following stacks into each *staging* account (management, delegated IAM Identity Center admin, non-delegated) 16 | * secrets.yaml - creates the secrets for storing the credentials for your test GSuite and IAM Identity Center instances 17 | * testing.yaml - creates the pipeline to deploy and test prior to raising a pull request. 18 | Make a note of the output values 19 | 20 | * Now setup your *production* account 21 | * Manually create your code star connection 22 | * release.yaml - creates the pipeline to build, trigger the test pipeline in staging and where appropriate publish the app 23 | 24 | 25 | -------------------------------------------------------------------------------- /cicd/cloudformation/developer.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Description: 5 | This CloudFormation template will deploy a full CI/CD pipeline for SSO 6 | Sync. It includes building with AWS CodeBuild, publishing to a 7 | staging (private) AWS Serverless Application Repository (SAR), and test within 8 | this same account via AWS CloudFormation. 9 | 10 | Parameters: 11 | 12 | RepoName: 13 | Description: The repo name on github 14 | Default: ssosync 15 | Type: String 16 | 17 | OwnerName: 18 | Description: The user/organization that owns the above repo 19 | Default: awslabs 20 | Type: String 21 | 22 | BranchName: 23 | Description: The branch name on github 24 | Default: master 25 | Type: String 26 | 27 | GitHubOAuthToken: 28 | Description: Create a token with 'repo' and 'admin:repo_hook' permissions here https://github.com/settings/tokens 29 | Type: String 30 | NoEcho: True 31 | 32 | SecretsConfig: 33 | Description: Output from the secrets.yaml stack 34 | Type: String 35 | AllowedPattern: '(arn:aws:secretsmanager:((us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d):[0-9]{8,12}:secret:[a-zA-Z0-9/_+=.@-]{1,512})(,(arn:aws:secretsmanager:((us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d):[0-9]{8,12}:secret:[a-zA-Z0-9/_+=.@-]{1,512})){3}((,arn:aws:secretsmanager:((us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d):[0-9]{8,12}:secret:[a-zA-Z0-9/_+=.@-]{1,512})|(,"")){4}(,arn:aws:kms:((us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d):[0-9]{12}:key/[a-zA-Z0-9/_+=.@-]{1,512})' 36 | 37 | 38 | Metadata: 39 | AWS::CloudFormation::Interface: 40 | ParameterGroups: 41 | - Label: 42 | default: GitHub Repo 43 | Parameters: 44 | - OwnerName 45 | - RepoName 46 | - BranchName 47 | - Label: 48 | default: GitHub Auth 49 | Parameters: 50 | - GitHubOAuthToken 51 | 52 | ParameterLabels: 53 | RepoName: 54 | default: "Repository Name" 55 | OwnerName: 56 | default: "Repository Owner" 57 | BranchName: 58 | default: "Branch Name" 59 | GitHubOAuthToken: 60 | default: "GitHub OAuth Token" 61 | SecretsConfig: 62 | default: "TestConfig from the output of secrets.yaml" 63 | 64 | Resources: 65 | CodePipelineLogGroup: 66 | Type: AWS::Logs::LogGroup 67 | UpdateReplacePolicy: Delete 68 | Properties: 69 | RetentionInDays: 30 70 | 71 | ArtifactBucketKey: 72 | Type: AWS::KMS::Key 73 | Properties: 74 | Description: Key for this CodePipeline 75 | Enabled: true 76 | KeySpec: SYMMETRIC_DEFAULT 77 | KeyUsage: ENCRYPT_DECRYPT 78 | MultiRegion: false 79 | PendingWindowInDays: 7 80 | KeyPolicy: 81 | Version: 2012-10-17 82 | Id: key-default-1 83 | Statement: 84 | - Sid: Enable IAM User Permissions 85 | Effect: Allow 86 | Principal: 87 | AWS: !Sub arn:aws:iam::${AWS::AccountId}:root 88 | Action: 'kms:*' 89 | Resource: '*' 90 | 91 | ArtifactBucket: 92 | Type: AWS::S3::Bucket 93 | UpdateReplacePolicy: Delete 94 | Properties: 95 | BucketEncryption: 96 | ServerSideEncryptionConfiguration: 97 | - ServerSideEncryptionByDefault: 98 | SSEAlgorithm: 'aws:kms' 99 | KMSMasterKeyID: !GetAtt ArtifactBucketKey.Arn 100 | BucketKeyEnabled: true 101 | 102 | ArtifactBucketPolicy: 103 | Type: AWS::S3::BucketPolicy 104 | Properties: 105 | Bucket: !Ref ArtifactBucket 106 | PolicyDocument: 107 | Version: '2012-10-17' 108 | Statement: 109 | - Action: ['s3:GetObject'] 110 | Effect: Allow 111 | Principal: 112 | Service: 'serverlessrepo.amazonaws.com' 113 | Resource: 114 | - !Sub ${ArtifactBucket.Arn}/* 115 | Condition: 116 | StringEquals: 117 | aws:SourceAccount: !Ref AWS::AccountId 118 | - Sid: DenyUnEncryptedObjectUploads 119 | Effect: Deny 120 | Principal: "*" 121 | Action: s3:PutObject 122 | Resource: !Sub ${ArtifactBucket.Arn}/* 123 | Condition: 124 | StringNotEquals: 125 | s3:x-amz-server-side-encryption: aws:kms 126 | - Sid: RequireKMSEncryption 127 | Effect: Deny 128 | Principal: "*" 129 | Action: s3:PutObject 130 | Resource: !Sub ${ArtifactBucket.Arn}/* 131 | Condition: 132 | StringNotLikeIfExists: 133 | s3:x-amz-server-side-encryption-aws-kms-key-id: !GetAtt ArtifactBucketKey.Arn 134 | - Sid: DenyInsecureConnections 135 | Effect: Deny 136 | Principal: "*" 137 | Action: "s3:*" 138 | Resource: !Sub ${ArtifactBucket.Arn}/* 139 | Condition: 140 | Bool: 141 | aws:SecureTransport: false 142 | 143 | AppBucket: 144 | Type: AWS::S3::Bucket 145 | UpdateReplacePolicy: Delete 146 | 147 | AppBucketPolicy: 148 | Type: AWS::S3::BucketPolicy 149 | Properties: 150 | Bucket: !Ref AppBucket 151 | PolicyDocument: 152 | Version: '2012-10-17' 153 | Statement: 154 | - Action: ['s3:GetObject'] 155 | Effect: Allow 156 | Principal: 157 | Service: 'serverlessrepo.amazonaws.com' 158 | Resource: 159 | - !Sub ${AppBucket.Arn}/* 160 | Condition: 161 | StringEquals: 162 | aws:SourceAccount: !Ref AWS::AccountId 163 | 164 | CodePipeline: 165 | Type: AWS::CodePipeline::Pipeline 166 | Properties: 167 | Name: SSOSync 168 | RoleArn: !Sub ${CodePipelineRole.Arn} 169 | ArtifactStore: 170 | Type: S3 171 | Location: !Ref ArtifactBucket 172 | EncryptionKey: 173 | Type: KMS 174 | Id: !GetAtt ArtifactBucketKey.Arn 175 | Stages: 176 | - Name: Source 177 | Actions: 178 | - Name: GitHub 179 | Namespace: GitHub 180 | ActionTypeId: 181 | Category: Source 182 | Owner: ThirdParty 183 | Version: 1 184 | Provider: GitHub 185 | OutputArtifacts: 186 | - Name: Source 187 | Configuration: 188 | Owner: !Ref OwnerName 189 | Repo: !Ref RepoName 190 | Branch: !Ref BranchName 191 | OAuthToken: !Ref GitHubOAuthToken 192 | - Name: App 193 | Actions: 194 | - Name: Git-Metadata 195 | Namespace: Git 196 | InputArtifacts: 197 | - Name: Source 198 | ActionTypeId: 199 | Category: Build 200 | Owner: AWS 201 | Version: 1 202 | Provider: CodeBuild 203 | OutputArtifacts: 204 | - Name: GitVars 205 | RunOrder: '1' 206 | Configuration: 207 | ProjectName: !Ref GitMetadata 208 | PrimarySource: Source 209 | - Name: GoLang-Build 210 | InputArtifacts: 211 | - Name: Source 212 | ActionTypeId: 213 | Category: Build 214 | Owner: AWS 215 | Version: 1 216 | Provider: CodeBuild 217 | OutputArtifacts: 218 | - Name: Built 219 | RunOrder: '2' 220 | Configuration: 221 | ProjectName: !Ref CodeBuildApp 222 | PrimarySource: Source 223 | EnvironmentVariables: '[{"name":"GitTag","value":"#{Git.Tag}","type":"PLAINTEXT"},{"name":"GitCommit","value":"#{Git.CommitId}","type":"PLAINTEXT"}]' 224 | - Name: SAM-Package-SAR-Stage 225 | ActionTypeId: 226 | Category: Build 227 | Owner: AWS 228 | Version: 1 229 | Provider: CodeBuild 230 | RunOrder: 3 231 | Configuration: 232 | ProjectName: !Ref CodeBuildPackage 233 | PrimarySource: Source 234 | EnvironmentVariables: '[{"name":"GitTag","value":"#{Git.Tag}","type":"PLAINTEXT"},{"name":"GitVersionHash","value":"#{Git.CommitHash}","type":"PLAINTEXT"}]' 235 | OutputArtifacts: 236 | - Name: Packaged 237 | InputArtifacts: 238 | - Name: Built 239 | - Name: Source 240 | - Name: Deploy 241 | Actions: 242 | - Name: Staging 243 | Namespace: RC 244 | ActionTypeId: 245 | Category: Build 246 | Owner: AWS 247 | Version: 1 248 | Provider: CodeBuild 249 | RunOrder: 1 250 | Configuration: 251 | ProjectName: !Ref CodeBuildStaging 252 | PrimarySource: Source 253 | EnvironmentVariables: '[{"name":"GitTag","value":"#{Git.Tag}","type":"PLAINTEXT"},{"name":"GitVersionHash","value":"#{Git.CommitHash}","type":"PLAINTEXT"}]' 254 | OutputArtifacts: 255 | - Name: Tests 256 | InputArtifacts: 257 | - Name: Source 258 | - Name: Packaged 259 | - Name: Built 260 | - Name: Deploy 261 | ActionTypeId: 262 | Category: Deploy 263 | Owner: AWS 264 | Version: '1' 265 | Provider: CloudFormation 266 | Configuration: 267 | ActionMode: CREATE_UPDATE 268 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND,CAPABILITY_NAMED_IAM 269 | StackName: TestAccountExecution 270 | RoleArn: !GetAtt [CloudFormationDeployerRole, Arn] 271 | TemplateConfiguration: 'Tests::deploy/developer.json' 272 | TemplatePath: !Sub 'Tests::deploy/stack.yml' 273 | InputArtifacts: 274 | - Name: Tests 275 | RunOrder: 2 276 | - Name: SmokeTests 277 | Actions: 278 | - Name: Lambda 279 | ActionTypeId: 280 | Category: Test 281 | Owner: AWS 282 | Version: 1 283 | Provider: CodeBuild 284 | RunOrder: 1 285 | Configuration: 286 | ProjectName: !Ref CodeBuildSmokeLambda 287 | PrimarySource: Source 288 | OutputArtifacts: 289 | - Name: SmokeLambda 290 | InputArtifacts: 291 | - Name: Tests 292 | - Name: CLI 293 | ActionTypeId: 294 | Category: Test 295 | Owner: AWS 296 | Version: 1 297 | Provider: CodeBuild 298 | RunOrder: 2 299 | Configuration: 300 | ProjectName: !Ref CodeBuildSmokeCLI 301 | OutputArtifacts: 302 | - Name: SmokeCLI 303 | InputArtifacts: 304 | - Name: Tests 305 | - Name: CodePipeline 306 | ActionTypeId: 307 | Category: Invoke 308 | Owner: AWS 309 | Version: 1 310 | Provider: Lambda 311 | RunOrder: 3 312 | Configuration: 313 | FunctionName: SSOSyncFunction 314 | OutputArtifacts: 315 | - Name: SmokeCodePipeline 316 | InputArtifacts: 317 | - Name: Tests 318 | - Name: CleanUp 319 | Actions: 320 | - Name: RemoveStack 321 | ActionTypeId: 322 | Category: Deploy 323 | Owner: AWS 324 | Version: 1 325 | Provider: CloudFormation 326 | Configuration: 327 | ActionMode: DELETE_ONLY 328 | StackName: TestAccountExecution 329 | RoleArn: !GetAtt [CloudFormationDeployerRole, Arn] 330 | InputArtifacts: 331 | - Name: Tests 332 | RunOrder: 1 333 | 334 | 335 | GitMetadata: 336 | Type: AWS::CodeBuild::Project 337 | Properties: 338 | Name: SSOSync-Git-Metadata 339 | Description: Build project for SSOSync 340 | ServiceRole: !Ref CodeBuildAppRole 341 | Source: 342 | Type: CODEPIPELINE 343 | BuildSpec: "cicd/build/gitvars/buildspec.yml" 344 | Environment: 345 | ComputeType: BUILD_GENERAL1_SMALL 346 | Image: aws/codebuild/standard:7.0 347 | Type: LINUX_CONTAINER 348 | EnvironmentVariables: 349 | - Name: ARTIFACT_S3_BUCKET 350 | Value: !Sub ${ArtifactBucket} 351 | - Name: GitRepo 352 | Value: "https://github.com/awslabs/ssosync" 353 | - Name: GitBranch 354 | Value: !Ref BranchName 355 | Artifacts: 356 | Name: SSOSync 357 | Type: CODEPIPELINE 358 | LogsConfig: 359 | CloudWatchLogs: 360 | GroupName: !Ref CodePipelineLogGroup 361 | StreamName: !Ref GitMetadataLogs 362 | Status: ENABLED 363 | 364 | GitMetadataLogs: 365 | Type: AWS::Logs::LogStream 366 | Properties: 367 | LogGroupName: !Ref CodePipelineLogGroup 368 | LogStreamName: SSOSync-Git-Metadata 369 | 370 | CodeBuildApp: 371 | Type: AWS::CodeBuild::Project 372 | Properties: 373 | Name: SSOSync-Build-App 374 | Description: Build project for SSOSync 375 | ServiceRole: !Ref CodeBuildAppRole 376 | Source: 377 | Type: CODEPIPELINE 378 | BuildSpec: "cicd/build/build/buildspec.yml" 379 | Environment: 380 | ComputeType: BUILD_GENERAL1_SMALL 381 | Image: aws/codebuild/standard:7.0 382 | Type: LINUX_CONTAINER 383 | EnvironmentVariables: 384 | - Name: ARTIFACT_S3_BUCKET 385 | Value: !Sub ${ArtifactBucket} 386 | - Name: OUTPUT 387 | Value: main 388 | - Name: APP_NAME 389 | Value: ssosync 390 | Artifacts: 391 | Name: SSOSync 392 | Type: CODEPIPELINE 393 | LogsConfig: 394 | CloudWatchLogs: 395 | GroupName: !Ref CodePipelineLogGroup 396 | StreamName: !Ref CodeBuildAppLogs 397 | Status: ENABLED 398 | 399 | CodeBuildAppLogs: 400 | Type: AWS::Logs::LogStream 401 | Properties: 402 | LogGroupName: !Ref CodePipelineLogGroup 403 | LogStreamName: SSOSync-Build-App 404 | 405 | CodeBuildPackage: 406 | Type: AWS::CodeBuild::Project 407 | Properties: 408 | Name: SSOSync-Package 409 | Description: SAM package for SSOSync 410 | ServiceRole: !Ref CodeBuildPackageRole 411 | Source: 412 | Type: CODEPIPELINE 413 | BuildSpec: "cicd/build/package/buildspec.yml" 414 | Environment: 415 | ComputeType: BUILD_GENERAL1_SMALL 416 | Image: aws/codebuild/standard:7.0 417 | Type: LINUX_CONTAINER 418 | EnvironmentVariables: 419 | - Name: ARTIFACT_S3_BUCKET 420 | Value: !Sub ${ArtifactBucket} 421 | - Name: S3Bucket 422 | Value: !Ref AppBucket 423 | - Name: Template 424 | Value: template.yaml 425 | Artifacts: 426 | Name: SSOSync 427 | Type: CODEPIPELINE 428 | LogsConfig: 429 | CloudWatchLogs: 430 | GroupName: !Ref CodePipelineLogGroup 431 | StreamName: !Ref CodeBuildPackageLogs 432 | Status: ENABLED 433 | 434 | CodeBuildPackageLogs: 435 | Type: AWS::Logs::LogStream 436 | Properties: 437 | LogGroupName: !Ref CodePipelineLogGroup 438 | LogStreamName: SSOSync-Package 439 | 440 | CodeBuildStaging: 441 | Type: AWS::CodeBuild::Project 442 | Properties: 443 | Name: SSOSync-Staging 444 | Description: Publish SSOSync to Serverless Application Repository in Staging 445 | ServiceRole: !Ref CodeBuildPublishRole 446 | Source: 447 | Type: CODEPIPELINE 448 | BuildSpec: "cicd/account_execution/staging/buildspec.yml" 449 | Environment: 450 | ComputeType: BUILD_GENERAL1_SMALL 451 | Image: aws/codebuild/standard:7.0 452 | Type: LINUX_CONTAINER 453 | EnvironmentVariables: 454 | - Name: ARTIFACT_S3_BUCKET 455 | Value: !Sub ${ArtifactBucket} 456 | - Name: AppArn 457 | Value: !Sub "arn:aws:serverlessrepo:${AWS::Region}:${AWS::AccountId}:applications/SSOSync-Staging" 458 | - Name: SecretSCIMEndpoint 459 | Value: !Select [0, !Split [',', !Ref SecretsConfig]] 460 | - Name: SecretSCIMAccessToken 461 | Value: !Select [1, !Split [',', !Ref SecretsConfig]] 462 | - Name: SecretRegion 463 | Value: !Select [2, !Split [',', !Ref SecretsConfig]] 464 | - Name: SecretIdentityStoreID 465 | Value: !Select [3, !Split [',', !Ref SecretsConfig]] 466 | - Name: SecretGoogleCredentials 467 | Value: !Select [4, !Split [',', !Ref SecretsConfig]] 468 | - Name: SecretGoogleAdminEmail 469 | Value: !Select [5, !Split [',', !Ref SecretsConfig]] 470 | - Name: SecretWIFClientLibraryConfig 471 | Value: !Select [6, !Split [',', !Ref SecretsConfig]] 472 | - Name: SecretWIFServiceAccountEmail 473 | Value: !Select [7, !Split [',', !Ref SecretsConfig]] 474 | - Name: KeyForSecrets 475 | Value: !Select [8, !Split [',', !Ref SecretsConfig]] 476 | Artifacts: 477 | Name: SSOSync 478 | Type: CODEPIPELINE 479 | LogsConfig: 480 | CloudWatchLogs: 481 | GroupName: !Ref CodePipelineLogGroup 482 | StreamName: !Ref CodeBuildStagingLogs 483 | Status: ENABLED 484 | 485 | CodeBuildStagingLogs: 486 | Type: AWS::Logs::LogStream 487 | Properties: 488 | LogGroupName: !Ref CodePipelineLogGroup 489 | LogStreamName: SSOSync-Staging 490 | 491 | CodeBuildSmokeCLI: 492 | Type: AWS::CodeBuild::Project 493 | Properties: 494 | Name: SSOSync-Smoke-CLI 495 | Description: "Execute within a container on the cli to prove cli invokation" 496 | ServiceRole: !Ref CodeBuildTestRole 497 | Source: 498 | Type: CODEPIPELINE 499 | BuildSpec: "tests/account_execution/cli/buildspec.yml" 500 | Environment: 501 | ComputeType: BUILD_GENERAL1_SMALL 502 | Image: aws/codebuild/standard:7.0 503 | Type: LINUX_CONTAINER 504 | Artifacts: 505 | Name: SSOSync 506 | Type: CODEPIPELINE 507 | LogsConfig: 508 | CloudWatchLogs: 509 | GroupName: !Ref CodePipelineLogGroup 510 | StreamName: !Ref CodeBuildSmokeCLILogs 511 | Status: ENABLED 512 | 513 | CodeBuildSmokeCLILogs: 514 | Type: AWS::Logs::LogStream 515 | Properties: 516 | LogGroupName: !Ref CodePipelineLogGroup 517 | LogStreamName: SSOSync-Smoke-CLI 518 | 519 | CodeBuildSmokeLambda: 520 | Type: AWS::CodeBuild::Project 521 | Properties: 522 | Name: SSOSync-Smoke-Lambda 523 | Description: "Execute Lambda from within a container, to test invokation without codepipeline event handling" 524 | ServiceRole: !Ref CodeBuildTestRole 525 | Source: 526 | Type: CODEPIPELINE 527 | BuildSpec: "tests/account_execution/lambda/buildspec.yml" 528 | Environment: 529 | ComputeType: BUILD_GENERAL1_SMALL 530 | Image: aws/codebuild/standard:7.0 531 | Type: LINUX_CONTAINER 532 | Artifacts: 533 | Name: SSOSync 534 | Type: CODEPIPELINE 535 | LogsConfig: 536 | CloudWatchLogs: 537 | GroupName: !Ref CodePipelineLogGroup 538 | StreamName: !Ref CodeBuildSmokeLambdaLogs 539 | Status: ENABLED 540 | 541 | CodeBuildSmokeLambdaLogs: 542 | Type: AWS::Logs::LogStream 543 | Properties: 544 | LogGroupName: !Ref CodePipelineLogGroup 545 | LogStreamName: SSOSync-Smoke-Lambda 546 | 547 | 548 | CodePipelineRole: 549 | Type: AWS::IAM::Role 550 | Properties: 551 | RoleName: !Sub SSOSync-CodePipeline-${AWS::Region} 552 | AssumeRolePolicyDocument: 553 | Version: '2012-10-17' 554 | Statement: 555 | - Action: ['sts:AssumeRole'] 556 | Effect: Allow 557 | Principal: 558 | Service: [codepipeline.amazonaws.com] 559 | Path: / 560 | Policies: 561 | - PolicyName: !Sub SSOSync-CodePipeline-${AWS::Region} 562 | PolicyDocument: 563 | Version: '2012-10-17' 564 | Statement: 565 | - Action: 566 | - 'iam:PassRole' 567 | Effect: Allow 568 | Resource: '*' 569 | - Action: 570 | - 'codebuild:BatchGetBuilds' 571 | - 'codebuild:StartBuild' 572 | Resource: 573 | - !Sub ${GitMetadata.Arn} 574 | - !Sub ${CodeBuildApp.Arn} 575 | - !Sub ${CodeBuildPackage.Arn} 576 | - !Sub ${CodeBuildStaging.Arn} 577 | - !Sub ${CodeBuildSmokeCLI.Arn} 578 | - !Sub ${CodeBuildSmokeLambda.Arn} 579 | Effect: Allow 580 | - Action: 581 | - 's3:GetBucketPolicy' 582 | - 's3:GetBucketVersioning' 583 | Resource: 584 | - !Sub ${ArtifactBucket.Arn} 585 | Effect: Allow 586 | - Action: 587 | - 's3:*' 588 | Resource: 589 | - !Sub ${ArtifactBucket.Arn}/* 590 | Effect: Allow 591 | - Action: 592 | - 'kms:DescribeKey' 593 | - 'kms:GenerateDataKey*' 594 | - 'kms:Encrypt' 595 | - 'kms:ReEncrypt*' 596 | - 'kms:Decrypt' 597 | Effect: Allow 598 | Resource: 599 | - !GetAtt ArtifactBucketKey.Arn 600 | - Action: 601 | - 'lambda:InvokeFunction' 602 | Resource: 603 | - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:SSOSyncFunction 604 | Effect: Allow 605 | - Action: 606 | - 'cloudformation:CreateStack' 607 | - 'cloudformation:DescribeStacks' 608 | - 'cloudformation:DeleteStack' 609 | - 'cloudformation:UpdateStack' 610 | - 'cloudformation:CreateChangeSet' 611 | - 'cloudformation:ExecuteChangeSet' 612 | - 'cloudformation:DeleteChangeSet' 613 | - 'cloudformation:DescribeChangeSet' 614 | - 'cloudformation:SetStackPolicy' 615 | Resource: 616 | - '*' 617 | Effect: Allow 618 | 619 | CodeBuildAppRole: 620 | Type: AWS::IAM::Role 621 | Properties: 622 | RoleName: !Sub SSOSync-CodeBuild-App-${AWS::Region} 623 | AssumeRolePolicyDocument: 624 | Version: '2012-10-17' 625 | Statement: 626 | - Action: ['sts:AssumeRole'] 627 | Effect: Allow 628 | Principal: 629 | Service: [codebuild.amazonaws.com] 630 | Path: / 631 | Policies: 632 | - PolicyName: !Sub SSOSync-CodeBuild-App-${AWS::Region} 633 | PolicyDocument: 634 | Version: '2012-10-17' 635 | Statement: 636 | - Action: 637 | - 'logs:CreateLogGroup' 638 | - 'logs:CreateLogStream' 639 | - 'logs:PutLogEvents' 640 | Effect: Allow 641 | Resource: '*' 642 | - Action: 643 | - 'kms:DescribeKey' 644 | - 'kms:GenerateDataKey*' 645 | - 'kms:Encrypt' 646 | - 'kms:ReEncrypt*' 647 | - 'kms:Decrypt' 648 | Effect: Allow 649 | Resource: 650 | - !GetAtt ArtifactBucketKey.Arn 651 | - Action: 's3:*' 652 | Effect: Allow 653 | Resource: 654 | - !Sub ${ArtifactBucket.Arn}/* 655 | 656 | CodeBuildPackageRole: 657 | Type: AWS::IAM::Role 658 | Properties: 659 | RoleName: !Sub SSOSync-CodeBuild-Package-${AWS::Region} 660 | AssumeRolePolicyDocument: 661 | Version: '2012-10-17' 662 | Statement: 663 | - Action: ['sts:AssumeRole'] 664 | Effect: Allow 665 | Principal: 666 | Service: [codebuild.amazonaws.com] 667 | Path: / 668 | Policies: 669 | - PolicyName: !Sub SSOSync-CodeBuild-Package-${AWS::Region} 670 | PolicyDocument: 671 | Version: '2012-10-17' 672 | Statement: 673 | - Action: 674 | - 'logs:CreateLogGroup' 675 | - 'logs:CreateLogStream' 676 | - 'logs:PutLogEvents' 677 | Effect: Allow 678 | Resource: '*' 679 | - Action: 680 | - 'kms:DescribeKey' 681 | - 'kms:GenerateDataKey*' 682 | - 'kms:Encrypt' 683 | - 'kms:ReEncrypt*' 684 | - 'kms:Decrypt' 685 | Effect: Allow 686 | Resource: 687 | - !GetAtt ArtifactBucketKey.Arn 688 | - Action: 's3:*' 689 | Effect: Allow 690 | Resource: 691 | - !Sub ${ArtifactBucket.Arn}/* 692 | - !Sub ${AppBucket.Arn}/* 693 | - Action: 'serverlessrepo:*' 694 | Effect: Allow 695 | Resource: 696 | - !Sub arn:aws:serverlessrepo:${AWS::Region}:${AWS::AccountId}:applications/* 697 | - Action: 698 | - 'ssm:GetParameters' 699 | - 'ssm:PutParameter' 700 | - 'ssm:DeleteParameter' 701 | - 'ssm:DeleteParameters' 702 | - 'ssm:DescribeParameters' 703 | Effect: Allow 704 | Resource: 705 | - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/SSOSync/* 706 | 707 | CodeBuildPublishRole: 708 | Type: AWS::IAM::Role 709 | Properties: 710 | RoleName: !Sub SSOSync-CodeBuild-Publish-${AWS::Region} 711 | AssumeRolePolicyDocument: 712 | Version: '2012-10-17' 713 | Statement: 714 | - Action: ['sts:AssumeRole'] 715 | Effect: Allow 716 | Principal: 717 | Service: [codebuild.amazonaws.com] 718 | Path: / 719 | Policies: 720 | - PolicyName: !Sub SSOSync-CodeBuild-Publish-${AWS::Region} 721 | PolicyDocument: 722 | Version: '2012-10-17' 723 | Statement: 724 | - Action: 725 | - 'logs:CreateLogGroup' 726 | - 'logs:CreateLogStream' 727 | - 'logs:PutLogEvents' 728 | Effect: Allow 729 | Resource: '*' 730 | - Action: 's3:*' 731 | Effect: Allow 732 | Resource: 733 | - !Sub ${ArtifactBucket.Arn}/* 734 | - !Sub ${AppBucket.Arn}/* 735 | - Action: 736 | - 'kms:DescribeKey' 737 | - 'kms:GenerateDataKey*' 738 | - 'kms:Encrypt' 739 | - 'kms:ReEncrypt*' 740 | - 'kms:Decrypt' 741 | Effect: Allow 742 | Resource: 743 | - !GetAtt ArtifactBucketKey.Arn 744 | - Action: 'serverlessrepo:*' 745 | Effect: Allow 746 | Resource: 747 | - !Sub arn:aws:serverlessrepo:${AWS::Region}:${AWS::AccountId}:applications/* 748 | - Action: 749 | - 'ssm:GetParameters' 750 | - 'ssm:PutParameter' 751 | - 'ssm:DeleteParameter' 752 | - 'ssm:DeleteParameters' 753 | - 'ssm:DescribeParameters' 754 | Effect: Allow 755 | Resource: 756 | - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/SSOSync/* 757 | 758 | CodeBuildTestRole: 759 | Type: AWS::IAM::Role 760 | Properties: 761 | RoleName: !Sub SSOSync-CodeBuild-Test-${AWS::Region} 762 | AssumeRolePolicyDocument: 763 | Version: '2012-10-17' 764 | Statement: 765 | - Action: ['sts:AssumeRole'] 766 | Effect: Allow 767 | Principal: 768 | Service: [codebuild.amazonaws.com] 769 | Path: / 770 | Policies: 771 | - PolicyName: !Sub SSOSync-CodeBuild-Test-${AWS::Region} 772 | PolicyDocument: 773 | Version: '2012-10-17' 774 | Statement: 775 | - Action: 776 | - 'logs:CreateLogGroup' 777 | - 'logs:CreateLogStream' 778 | - 'logs:PutLogEvents' 779 | Effect: Allow 780 | Resource: '*' 781 | - Action: 782 | - 'kms:DescribeKey' 783 | - 'kms:GenerateDataKey*' 784 | - 'kms:Encrypt' 785 | - 'kms:ReEncrypt*' 786 | - 'kms:Decrypt' 787 | Effect: Allow 788 | Resource: 789 | - !GetAtt ArtifactBucketKey.Arn 790 | - Action: 's3:*' 791 | Effect: Allow 792 | Resource: 793 | - !Sub ${ArtifactBucket.Arn}/* 794 | - Action: 795 | - 'lambda:invokeFunction' 796 | Effect: Allow 797 | Resource: '*' 798 | - Action: 799 | - "identitystore:DeleteUser" 800 | - "identitystore:DeleteGroup" 801 | - "identitystore:CreateGroup" 802 | - "identitystore:CreateGroupMembership" 803 | - "identitystore:ListGroups" 804 | - "identitystore:ListUsers" 805 | - "identitystore:ListGroupMemberships" 806 | - "identitystore:IsMemberInGroups" 807 | - "identitystore:GetGroupMembershipId" 808 | - "identitystore:DeleteGroupMembership" 809 | Effect: Allow 810 | Resource: '*' 811 | - Action: 812 | - "secretsmanager:Get*" 813 | Effect: Allow 814 | Resource: '*' 815 | 816 | CloudFormationDeployerRole: 817 | Type: AWS::IAM::Role 818 | Properties: 819 | RoleName: !Sub SSOSync-CloudFormationDeployerRole-${AWS::Region} 820 | AssumeRolePolicyDocument: 821 | Statement: 822 | - Action: ['sts:AssumeRole'] 823 | Effect: Allow 824 | Principal: 825 | Service: [cloudformation.amazonaws.com] 826 | - Action: ['sts:AssumeRole'] 827 | Effect: Allow 828 | Principal: 829 | AWS: !Ref AWS::AccountId 830 | - Action: ['sts:AssumeRole'] 831 | Effect: Allow 832 | Principal: 833 | Service: [codepipeline.amazonaws.com] 834 | Version: '2012-10-17' 835 | Path: / 836 | Policies: 837 | - PolicyName: CloudFormation-Deployer-Policy 838 | PolicyDocument: 839 | Version: '2012-10-17' 840 | Statement: 841 | - Action: '*' 842 | Effect: Allow 843 | Resource: '*' 844 | 845 | 846 | -------------------------------------------------------------------------------- /cicd/cloudformation/secrets.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Description: 4 | This CloudFormation template will deploy a an IAM role and some Secrets to 5 | allow the CI/CD pipeline in the production account to deploy candidate releases 6 | (via privately shared app in the AWS Serverless Application Repository (SAR). 7 | 8 | Parameters: 9 | ManagementAccount: 10 | Description: AWS Account where staging build is automatically deployed and tested 11 | Type: String 12 | AllowedPattern: '[0-9]+' 13 | 14 | DelegatedAccount: 15 | Description: AWS Account where staging build is automatically deployed and tested 16 | Type: String 17 | AllowedPattern: '[0-9]+' 18 | 19 | NonDelegatedAccount: 20 | Description: AWS Account where staging build is automatically deployed and tested 21 | Type: String 22 | AllowedPattern: '[0-9]+' 23 | 24 | GoogleAuthMethod: 25 | Type: String 26 | AllowedValues: 27 | - Google Credentials 28 | - Workload Identity Federation 29 | - Both 30 | Default: "Google Credentials" 31 | 32 | GoogleCredentials: 33 | Description: Google Workspaces Credentials File, to log into Google (content of credentials.json) 34 | Type: String 35 | AllowedPattern: '(?!.*\s)|(\{(\s)*(".*")(\s)*:(\s)*(".*")(\s)*\})' 36 | NoEcho: true 37 | 38 | GoogleAdminEmail: 39 | Description: Google Workspaces Admin email 40 | Type: String 41 | AllowedPattern: '(?!.*\s)|(([a-zA-Z0-9.+=_-]{0,61})@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)' 42 | NoEcho: true 43 | 44 | WIFServiceAccountEmail: 45 | Description: Workload Identity Federation, the email address of service account used to impersonate a user using 46 | Type: String 47 | AllowedPattern: '(?!.*\s)|(([a-zA-Z0-9.+=_-]{0,61})@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)' 48 | NoEcho: true 49 | 50 | WIFClientLibraryConfig: 51 | Description: Workload Identity Federation, the client library config file for the provider (AWS Account) (contents of clientLibraryConfig-provider.json) 52 | Type: String 53 | AllowedPattern: '(?!.*\s)|(\{(\s)*(".*")(\s)*:(\s)*(".*")(\s)*\})' 54 | NoEcho: true 55 | 56 | SCIMEndpointUrl: 57 | Description: AWS IAM Identity Center SCIM Endpoint Url 58 | Type: String 59 | AllowedPattern: "https://scim.(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-([0-9]{1}).amazonaws.com/([A-Za-z0-9]{11})-([A-Za-z0-9]{4})-([A-Za-z0-9]{4})-([A-Za-z0-9]{4})-([A-Za-z0-9]{12})/scim/v2/?" 60 | NoEcho: true 61 | 62 | SCIMEndpointAccessToken: 63 | Description: AWS IAM Identity Center SCIM AccessToken 64 | Type: String 65 | AllowedPattern: '([0-9a-zA-Z/=+-\\]{500,620})' 66 | NoEcho: true 67 | 68 | IdentityStoreId: 69 | Description: The Id of the Identity Store for the AWS IAM Identity Center instance see (settings page) 70 | Type: String 71 | AllowedPattern: "d-[1-z0-9]{10}" 72 | 73 | 74 | Metadata: 75 | AWS::CloudFormation::Interface: 76 | ParameterGroups: 77 | - Label: 78 | default: Test Environments 79 | Parameters: 80 | - ManagementAccount 81 | - DelegatedAccount 82 | - NonDelegatedAccount 83 | - Label: 84 | default: Google Authentication Method 85 | Parameters: 86 | - GoogleAuthMethod 87 | - Label: 88 | default: Parameters for Google Credentials based authentication, required if either Google Credentials or Both have been selected for Google Authentication Method 89 | Parameters: 90 | - GoogleAdminEmail 91 | - GoogleCredentials 92 | - Label: 93 | default: Parameters for Workload Identity Federation based authentication, required if either Workload Identity Federation or Both have been selected for Google Authentication Method 94 | Parameters: 95 | - WIFServiceAccountEmail 96 | - WIFClientLibraryConfig 97 | - Label: 98 | default: AWS IAM Identity Center 99 | Parameters: 100 | - SCIMEndpointUrl 101 | - SCIMEndpointAccessToken 102 | - IdentityStoreId 103 | 104 | ParameterLabels: 105 | ManagementAccount: 106 | default: "What is the account id of the Test Orgs, Management account?" 107 | DelegatedAccount: 108 | default: "What is the account id of the Test Orgs, Delegated admin account?" 109 | NonDelegatedAccount: 110 | default: "What is the account id of the Test Orgs, Non-delegated admin account?" 111 | GoogleAuthMethod: 112 | default: "Which Google Auth Methods do you want to test with?" 113 | GoogleCredentials: 114 | default: "contents of credentials.json" 115 | GoogleAdminEmail: 116 | default: "admin@WorkspaceDomain" 117 | WIFServiceAccountEmail: 118 | default: "service-account@@WorkspaceDomain" 119 | WIFClientLibraryConfig: 120 | default: "contents of clientLibraryConfig-provider.json" 121 | SCIMEndpointUrl: 122 | default: "https://scim..amazonaws.com//scim/v2/" 123 | SCIMEndpointAccessToken: 124 | default: "AWS SSO SCIM Access Token" 125 | IdentityStoreId: 126 | default: "The Identity Store Id" 127 | 128 | Conditions: 129 | CreateGoogle: !Or 130 | - !Equals 131 | - !Ref GoogleAuthMethod 132 | - "Google Credentials" 133 | - !Equals 134 | - !Ref GoogleAuthMethod 135 | - "Both" 136 | CreateWIF: !Or 137 | - !Equals 138 | - !Ref GoogleAuthMethod 139 | - "Workload Identity Federation" 140 | - !Equals 141 | - !Ref GoogleAuthMethod 142 | - "Both" 143 | 144 | GoogleCreds: !Equals 145 | - !Ref GoogleAuthMethod 146 | - "Google Credentials" 147 | WIFCreds: !Equals 148 | - !Ref GoogleAuthMethod 149 | - "Workload Identity Federation" 150 | BothCreds: !Equals 151 | - !Ref GoogleAuthMethod 152 | - "Both" 153 | 154 | Rules: 155 | # Fail when any assertion returns false 156 | # If they have selected Google Credentials then check they have provided valid data for GoogleCredentials 157 | GoogleCredentialsOnly: 158 | RuleCondition: !Or 159 | - !Equals 160 | - !Ref GoogleAuthMethod 161 | - "Google Credentials" 162 | - !Equals 163 | - !Ref GoogleAuthMethod 164 | - "Both" 165 | Assertions: 166 | - AssertDescription: You have selected Google Credentials, You need to provide a Google Admin email address. 167 | Assert: !Not 168 | - !Equals 169 | - !Ref GoogleAdminEmail 170 | - "" 171 | - AssertDescription: You have selected Google Credentials, You need to provide the content of a Credentials file (json). 172 | Assert: !Not 173 | - !Equals 174 | - !Ref GoogleCredentials 175 | - "" 176 | # If they have selected Workload Identity Federation, then check they have provide valid data for WIF 177 | WorkloadIdentityFederationOnly: 178 | RuleCondition: !Or 179 | - !Equals 180 | - !Ref GoogleAuthMethod 181 | - "Workload Identity Federation" 182 | - !Equals 183 | - !Ref GoogleAuthMethod 184 | - "Both" 185 | Assertions: 186 | - AssertDescription: You have selected Workload Identity Federation, You need to provide a Google Service Account email address. 187 | Assert: !Not 188 | - !Equals 189 | - !Ref WIFServiceAccountEmail 190 | - "" 191 | - AssertDescription: You have selected Workload Identity Federation, You need to provide the content of a Client Library Config file (json). 192 | Assert: !Not 193 | - !Equals 194 | - !Ref WIFClientLibraryConfig 195 | - "" 196 | 197 | Resources: 198 | KeyAlias: 199 | Type: AWS::KMS::Alias 200 | Properties: 201 | AliasName: alias/SSOSync 202 | TargetKeyId: !Ref KeyForSecrets 203 | 204 | KeyForSecrets: 205 | Type: AWS::KMS::Key 206 | DeletionPolicy: Retain 207 | UpdateReplacePolicy: Delete 208 | Properties: 209 | Description: Key for protecting SSOSync Secrets in cross-account deployment 210 | Enabled: true 211 | KeySpec: SYMMETRIC_DEFAULT 212 | KeyUsage: ENCRYPT_DECRYPT 213 | MultiRegion: false 214 | PendingWindowInDays: 7 215 | KeyPolicy: 216 | Version: 2012-10-17 217 | Id: key-default-1 218 | Statement: 219 | - Sid: Enable IAM User Permissions 220 | Effect: Allow 221 | Principal: 222 | AWS: !Sub arn:aws:iam::${AWS::AccountId}:root 223 | Action: 'kms:*' 224 | Resource: '*' 225 | - Sid: DeployRole in Management account 226 | Effect: Allow 227 | Principal: 228 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 229 | Action: 230 | - kms:Decrypt 231 | - kms:DescribeKey 232 | Resource: '*' 233 | - Sid: DeployRole in Delegated Admin account 234 | Effect: Allow 235 | Principal: 236 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 237 | Action: 238 | - kms:Decrypt 239 | - kms:DescribeKey 240 | Resource: '*' 241 | - Sid: DeployRole in Non Delegated Admin account 242 | Effect: Allow 243 | Principal: 244 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 245 | Action: 246 | - kms:Decrypt 247 | - kms:DescribeKey 248 | Resource: '*' 249 | 250 | SecretGoogleCredentials: 251 | Type: "AWS::SecretsManager::Secret" 252 | Condition: CreateGoogle 253 | DeletionPolicy: Retain 254 | UpdateReplacePolicy: Delete 255 | Properties: 256 | Name: PipelineGoogleCredentials 257 | SecretString: !Ref GoogleCredentials 258 | KmsKeyId: !Ref KeyAlias 259 | 260 | SecretGoogleCredentialsPolicy: 261 | Type: AWS::SecretsManager::ResourcePolicy 262 | Condition: CreateGoogle 263 | Properties: 264 | SecretId: !Ref SecretGoogleCredentials 265 | ResourcePolicy: 266 | Version: 2012-10-17 267 | Statement: 268 | - Effect: Allow 269 | Principal: 270 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 271 | Action: 272 | - secretsmanager:GetSecretValue 273 | Resource: '*' 274 | - Effect: Allow 275 | Principal: 276 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 277 | Action: 278 | - secretsmanager:GetSecretValue 279 | Resource: '*' 280 | - Effect: Allow 281 | Principal: 282 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 283 | Action: 284 | - secretsmanager:GetSecretValue 285 | Resource: '*' 286 | 287 | SecretGoogleAdminEmail: 288 | Type: "AWS::SecretsManager::Secret" 289 | Condition: CreateGoogle 290 | DeletionPolicy: Retain 291 | UpdateReplacePolicy: Delete 292 | Properties: 293 | Name: PipelineGoogleAdminEmail 294 | SecretString: !Ref GoogleAdminEmail 295 | KmsKeyId: !Ref KeyAlias 296 | 297 | SecretGoogleAdminEmailPolicy: 298 | Type: AWS::SecretsManager::ResourcePolicy 299 | Condition: CreateGoogle 300 | Properties: 301 | SecretId: !Ref SecretGoogleAdminEmail 302 | ResourcePolicy: 303 | Version: 2012-10-17 304 | Statement: 305 | - Effect: Allow 306 | Principal: 307 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 308 | Action: 309 | - secretsmanager:GetSecretValue 310 | Resource: '*' 311 | - Effect: Allow 312 | Principal: 313 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 314 | Action: 315 | - secretsmanager:GetSecretValue 316 | Resource: '*' 317 | - Effect: Allow 318 | Principal: 319 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 320 | Action: 321 | - secretsmanager:GetSecretValue 322 | Resource: '*' 323 | 324 | SecretWIFServiceAccountEmail: 325 | Type: "AWS::SecretsManager::Secret" 326 | Condition: CreateWIF 327 | DeletionPolicy: Retain 328 | UpdateReplacePolicy: Delete 329 | Properties: 330 | Name: PipelineWIFServiceAccountEmail 331 | SecretString: !Ref WIFServiceAccountEmail 332 | KmsKeyId: !Ref KeyAlias 333 | 334 | SecretWIFServiceAccountEmailPolicy: 335 | Type: AWS::SecretsManager::ResourcePolicy 336 | Condition: CreateWIF 337 | Properties: 338 | SecretId: !Ref SecretWIFServiceAccountEmail 339 | ResourcePolicy: 340 | Version: 2012-10-17 341 | Statement: 342 | - Effect: Allow 343 | Principal: 344 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 345 | Action: 346 | - secretsmanager:GetSecretValue 347 | Resource: '*' 348 | - Effect: Allow 349 | Principal: 350 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 351 | Action: 352 | - secretsmanager:GetSecretValue 353 | Resource: '*' 354 | - Effect: Allow 355 | Principal: 356 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 357 | Action: 358 | - secretsmanager:GetSecretValue 359 | Resource: '*' 360 | 361 | SecretWIFClientLibraryConfig: 362 | Type: "AWS::SecretsManager::Secret" 363 | Condition: CreateWIF 364 | DeletionPolicy: Retain 365 | UpdateReplacePolicy: Delete 366 | Properties: 367 | Name: PipelineWIFClientLibraryConfigSecret 368 | SecretString: !Ref WIFClientLibraryConfig 369 | KmsKeyId: !Ref KeyAlias 370 | 371 | SecretWIFClientLibraryConfigPolicy: 372 | Type: AWS::SecretsManager::ResourcePolicy 373 | Condition: CreateWIF 374 | Properties: 375 | SecretId: !Ref SecretWIFClientLibraryConfig 376 | ResourcePolicy: 377 | Version: 2012-10-17 378 | Statement: 379 | - Effect: Allow 380 | Principal: 381 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 382 | Action: 383 | - secretsmanager:GetSecretValue 384 | Resource: '*' 385 | - Effect: Allow 386 | Principal: 387 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 388 | Action: 389 | - secretsmanager:GetSecretValue 390 | Resource: '*' 391 | - Effect: Allow 392 | Principal: 393 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 394 | Action: 395 | - secretsmanager:GetSecretValue 396 | Resource: '*' 397 | 398 | SecretSCIMEndpoint: # This can be moved to custom provider 399 | Type: "AWS::SecretsManager::Secret" 400 | DeletionPolicy: Retain 401 | UpdateReplacePolicy: Delete 402 | Properties: 403 | Name: PipelineSCIMEndpointUrl 404 | SecretString: !Ref SCIMEndpointUrl 405 | KmsKeyId: !Ref KeyAlias 406 | 407 | SecretSCIMEndpointPolicy: 408 | Type: AWS::SecretsManager::ResourcePolicy 409 | Properties: 410 | SecretId: !Ref SecretSCIMEndpoint 411 | ResourcePolicy: 412 | Version: 2012-10-17 413 | Statement: 414 | - Effect: Allow 415 | Principal: 416 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 417 | Action: 418 | - secretsmanager:GetSecretValue 419 | Resource: '*' 420 | - Effect: Allow 421 | Principal: 422 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 423 | Action: 424 | - secretsmanager:GetSecretValue 425 | Resource: '*' 426 | - Effect: Allow 427 | Principal: 428 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 429 | Action: 430 | - secretsmanager:GetSecretValue 431 | Resource: '*' 432 | 433 | SecretSCIMAccessToken: # This can be moved to custom provider 434 | Type: "AWS::SecretsManager::Secret" 435 | DeletionPolicy: Retain 436 | UpdateReplacePolicy: Delete 437 | Properties: 438 | Name: PipelineSCIMAccessToken 439 | SecretString: !Ref SCIMEndpointAccessToken 440 | KmsKeyId: !Ref KeyAlias 441 | 442 | SecretSCIMAccessTokenPolicy: 443 | Type: AWS::SecretsManager::ResourcePolicy 444 | Properties: 445 | SecretId: !Ref SecretSCIMAccessToken 446 | ResourcePolicy: 447 | Version: 2012-10-17 448 | Statement: 449 | - Effect: Allow 450 | Principal: 451 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 452 | Action: 453 | - secretsmanager:GetSecretValue 454 | Resource: '*' 455 | - Effect: Allow 456 | Principal: 457 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 458 | Action: 459 | - secretsmanager:GetSecretValue 460 | Resource: '*' 461 | - Effect: Allow 462 | Principal: 463 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 464 | Action: 465 | - secretsmanager:GetSecretValue 466 | Resource: '*' 467 | 468 | SecretRegion: 469 | Type: "AWS::SecretsManager::Secret" 470 | DeletionPolicy: Retain 471 | UpdateReplacePolicy: Delete 472 | Properties: 473 | Name: PipelineRegion 474 | SecretString: !Select [1, !Split [".", !Ref SCIMEndpointUrl]] 475 | KmsKeyId: !Ref KeyAlias 476 | 477 | SecretRegionPolicy: 478 | Type: AWS::SecretsManager::ResourcePolicy 479 | Properties: 480 | SecretId: !Ref SecretRegion 481 | ResourcePolicy: 482 | Version: 2012-10-17 483 | Statement: 484 | - Effect: Allow 485 | Principal: 486 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 487 | Action: 488 | - secretsmanager:GetSecretValue 489 | Resource: '*' 490 | - Effect: Allow 491 | Principal: 492 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 493 | Action: 494 | - secretsmanager:GetSecretValue 495 | Resource: '*' 496 | - Effect: Allow 497 | Principal: 498 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 499 | Action: 500 | - secretsmanager:GetSecretValue 501 | Resource: '*' 502 | 503 | SecretIdentityStoreID: 504 | Type: "AWS::SecretsManager::Secret" 505 | DeletionPolicy: Retain 506 | UpdateReplacePolicy: Delete 507 | Properties: 508 | Name: PipelineIdentityStoreId 509 | SecretString: !Ref IdentityStoreId 510 | KmsKeyId: !Ref KeyAlias 511 | 512 | SecretIdentityStoreIDPolicy: 513 | Type: AWS::SecretsManager::ResourcePolicy 514 | Properties: 515 | SecretId: !Ref SecretIdentityStoreID 516 | ResourcePolicy: 517 | Version: 2012-10-17 518 | Statement: 519 | - Effect: Allow 520 | Principal: 521 | AWS: !Sub arn:aws:iam::${ManagementAccount}:root 522 | Action: 523 | - secretsmanager:GetSecretValue 524 | Resource: '*' 525 | - Effect: Allow 526 | Principal: 527 | AWS: !Sub arn:aws:iam::${DelegatedAccount}:root 528 | Action: 529 | - secretsmanager:GetSecretValue 530 | Resource: '*' 531 | - Effect: Allow 532 | Principal: 533 | AWS: !Sub arn:aws:iam::${NonDelegatedAccount}:root 534 | Action: 535 | - secretsmanager:GetSecretValue 536 | Resource: '*' 537 | Outputs: 538 | TestConfigGoogleCreds: 539 | Condition: GoogleCreds 540 | Description: "The Comma Separated list of Secrets and KMS Key ARNs to copy and paste into the CrossStackConfig field of the app for cross-account stack." 541 | Value: !Sub ${SecretSCIMEndpoint},${SecretSCIMAccessToken},${SecretRegion},${SecretIdentityStoreID},${SecretGoogleCredentials},${SecretGoogleAdminEmail},"","",arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/${KeyForSecrets} 542 | Export: 543 | Name: TestConfig 544 | 545 | TestConfigWIFCreds: 546 | Condition: WIFCreds 547 | Description: "The Comma Separated list of Secrets and KMS Key ARNs to copy and paste into the CrossStackConfig field of the app for cross-account stack." 548 | Value: !Sub ${SecretSCIMEndpoint},${SecretSCIMAccessToken},${SecretRegion},${SecretIdentityStoreID},"","",${SecretWIFClientLibraryConfig},${SecretWIFServiceAccountEmail},arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/${KeyForSecrets} 549 | Export: 550 | Name: TestConfig 551 | 552 | TestConfigBoth: 553 | Condition: BothCreds 554 | Description: "The Comma Separated list of Secrets and KMS Key ARNs to copy and paste into the CrossStackConfig field of the app for cross-account stack." 555 | Value: !Sub ${SecretSCIMEndpoint},${SecretSCIMAccessToken},${SecretRegion},${SecretIdentityStoreID},${SecretGoogleCredentials},${SecretGoogleAdminEmail},${SecretWIFClientLibraryConfig},${SecretWIFServiceAccountEmail},arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/${KeyForSecrets} 556 | Export: 557 | Name: TestConfig 558 | -------------------------------------------------------------------------------- /cicd/deploy_patterns/singlestack/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | interval: 10 6 | Success: '"Succeeded"' 7 | InProgress: '"InProgress"' 8 | Status: '"InProgress"' 9 | 10 | phases: 11 | 12 | pre_build: 13 | commands: 14 | # Print all environment variables (handy for AWS CodeBuild logs 15 | - env 16 | 17 | build: 18 | commands: 19 | # Create parameters 20 | - export AppVersion="${GitTag#v}-${GitVersionHash}" 21 | 22 | # Copy in the tests 23 | - cp -r cicd/tests ./ 24 | 25 | # Copy in the stack and params templates 26 | - mkdir deploy 27 | - cp cicd/deploy_patterns/singlestack/namedfunction.yml ./deploy/namedfunction.yml 28 | - cp cicd/deploy_patterns/singlestack/unnamedfunction.yml ./deploy/unnamedfunction.yml 29 | 30 | # Update params with the values for this run for a developer account 31 | - | 32 | jq -n \ 33 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:AWS*\"}" \ 34 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 35 | '$ARGS.named' > ./deploy/singlestack.json 36 | - cat ./deploy/singlestack.json 37 | 38 | 39 | post_build: 40 | commands: 41 | # zip up the content of TESTS 42 | - zip -r singlestack.zip ./tests 43 | - zip -r singlestack.zip ./deploy 44 | 45 | # Auth into the Staging Account 46 | - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role --role-arn "${StagingRole}" --role-session-name "CodePipelineRole" --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text)) 47 | 48 | # upload the zipfile to the S3 Bucket 49 | - aws s3 cp ./singlestack.zip s3://${TARGETS3BUCKET}/ 50 | 51 | # Start the test pipeline in the staging account 52 | - export ExecutionId=$(aws codepipeline start-pipeline-execution --name $pipeline --output text) 53 | - echo "ExecutionId=" $ExecutionId 54 | 55 | - | 56 | while expr "$Status" : "$InProgress" >/dev/null; do 57 | sleep $interval 58 | export Status="$(aws codepipeline get-pipeline-execution --pipeline-name $pipeline --output json --pipeline-execution-id $ExecutionId --query "pipelineExecution.status")" 59 | echo $Status 60 | done 61 | 62 | - echo "We are done" 63 | 64 | - | 65 | if expr "$Status" : "$Success" >/dev/null; then 66 | exit 0 67 | else 68 | exit 252 69 | fi 70 | 71 | artifacts: 72 | files: 73 | - deploy/**/* 74 | - tests/**/* 75 | -------------------------------------------------------------------------------- /cicd/deploy_patterns/singlestack/namedfunction.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Description: 5 | This CloudFormation template will deploy an instance of the SSOSync-Staging 6 | candidate releases (via privately shared app in the AWS Serverless Application 7 | Repository (SAR) within the Staging Account. 8 | 9 | Parameters: 10 | AppArn: 11 | Description: The candidate release in the SAR 12 | Default: 'arn:aws:serverlessrepo:::applications/' 13 | Type: String 14 | AppVersion: 15 | Description: The version of this build in SAR 16 | Default: 'v1.0.0-rc.10' 17 | Type: String 18 | GoogleAdminEmailArn: 19 | Type: String 20 | GoogleCredentialsArn: 21 | Type: String 22 | SCIMEndpointUrlArn: 23 | Type: String 24 | SCIMAccessTokenArn: 25 | Type: String 26 | RegionArn: 27 | Type: String 28 | IdentityStoreIdArn: 29 | Type: String 30 | GroupMatch: 31 | Description: The search string to match Groups in Google Workspace 32 | Default: 'name:AWS*' 33 | Type: String 34 | 35 | Resources: 36 | SARApp: 37 | Type: AWS::Serverless::Application 38 | Properties: 39 | Location: 40 | ApplicationId: !Ref AppArn 41 | SemanticVersion: !Ref AppVersion 42 | Parameters: 43 | FunctionName: SSOSyncFunction 44 | GoogleAdminEmail: !Join 45 | - '' 46 | - - '{{resolve:secretsmanager:' 47 | - !Ref GoogleAdminEmailArn 48 | - '}}' 49 | GoogleCredentials: !Join 50 | - '' 51 | - - '{{resolve:secretsmanager:' 52 | - !Ref GoogleCredentialsArn 53 | - '}}' 54 | SCIMEndpointUrl: !Join 55 | - '' 56 | - - '{{resolve:secretsmanager:' 57 | - !Ref SCIMEndpointUrlArn 58 | - '}}' 59 | SCIMEndpointAccessToken: !Join 60 | - '' 61 | - - '{{resolve:secretsmanager:' 62 | - !Ref SCIMAccessTokenArn 63 | - '}}' 64 | Region: !Join 65 | - '' 66 | - - '{{resolve:secretsmanager:' 67 | - !Ref RegionArn 68 | - '}}' 69 | IdentityStoreID: !Join 70 | - '' 71 | - - '{{resolve:secretsmanager:' 72 | - !Ref IdentityStoreIdArn 73 | - '}}' 74 | SyncMethod: groups 75 | GoogleGroupMatch: !Ref GroupMatch 76 | LogLevel: warn 77 | LogFormat: json 78 | -------------------------------------------------------------------------------- /cicd/deploy_patterns/singlestack/unnamedfunction.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Description: 5 | This CloudFormation template will deploy an instance of the SSOSync-Staging 6 | candidate releases (via privately shared app in the AWS Serverless Application 7 | Repository (SAR) within the Staging Account. 8 | 9 | Parameters: 10 | AppArn: 11 | Description: The candidate release in the SAR 12 | Default: 'arn:aws:serverlessrepo:::applications/' 13 | Type: String 14 | AppVersion: 15 | Description: The version of this build in SAR 16 | Default: 'v1.0.0-rc.10' 17 | Type: String 18 | GoogleAdminEmailArn: 19 | Type: String 20 | GoogleCredentialsArn: 21 | Type: String 22 | SCIMEndpointUrlArn: 23 | Type: String 24 | SCIMAccessTokenArn: 25 | Type: String 26 | RegionArn: 27 | Type: String 28 | IdentityStoreIdArn: 29 | Type: String 30 | GroupMatch: 31 | Description: The search string to match Groups in Google Workspace 32 | Default: 'name:AWS*' 33 | Type: String 34 | 35 | Resources: 36 | SARApp: 37 | Type: AWS::Serverless::Application 38 | Properties: 39 | Location: 40 | ApplicationId: !Ref AppArn 41 | SemanticVersion: !Ref AppVersion 42 | Parameters: 43 | GoogleAdminEmail: !Join 44 | - '' 45 | - - '{{resolve:secretsmanager:' 46 | - !Ref GoogleAdminEmailArn 47 | - '}}' 48 | GoogleCredentials: !Join 49 | - '' 50 | - - '{{resolve:secretsmanager:' 51 | - !Ref GoogleCredentialsArn 52 | - '}}' 53 | SCIMEndpointUrl: !Join 54 | - '' 55 | - - '{{resolve:secretsmanager:' 56 | - !Ref SCIMEndpointUrlArn 57 | - '}}' 58 | SCIMEndpointAccessToken: !Join 59 | - '' 60 | - - '{{resolve:secretsmanager:' 61 | - !Ref SCIMAccessTokenArn 62 | - '}}' 63 | Region: !Join 64 | - '' 65 | - - '{{resolve:secretsmanager:' 66 | - !Ref RegionArn 67 | - '}}' 68 | IdentityStoreID: !Join 69 | - '' 70 | - - '{{resolve:secretsmanager:' 71 | - !Ref IdentityStoreIdArn 72 | - '}}' 73 | SyncMethod: groups 74 | GoogleGroupMatch: !Ref GroupMatch 75 | LogLevel: warn 76 | LogFormat: json 77 | 78 | Outputs: 79 | SSOSyncArn: 80 | Description: "The Arn of the deployed lambda function" 81 | Value: 82 | Fn::GetAtt: 83 | - SARApp 84 | - Outputs.FunctionArn 85 | 86 | -------------------------------------------------------------------------------- /cicd/deploy_patterns/staging/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | ShareWith: "NOT-SHARED" 6 | 7 | phases: 8 | pre_build: 9 | commands: 10 | # Print all environment variables (handy for AWS CodeBuild logs 11 | - env 12 | 13 | # Copy in the package file 14 | - cp ${CODEBUILD_SRC_DIR_Packaged}/packaged-staging.yaml ./packaged.yaml 15 | 16 | # Check we have the required file 17 | - ls packaged.yaml 18 | 19 | build: 20 | commands: 21 | # Create parameters 22 | - export AppVersion="${GitTag#v}-${GitVersionHash}" 23 | - aws ssm put-parameter --name "/SSOSync/Staging/Version" --value ${AppVersion} --type String --overwrite 24 | 25 | # remove the previous builds 26 | #- aws serverlessrepo delete-application --application-id ${AppArn} 27 | 28 | # Package our application with AWS SAM 29 | - echo sam publish --template packaged.yaml --semantic-version ${AppVersion} 30 | - sam publish --template packaged.yaml --semantic-version ${AppVersion} 31 | 32 | # Share with the StagingAccount 33 | - | 34 | if expr "${ShareWith}" : "NOT-SHARED" >/dev/null; then 35 | echo "Skipping Sharing" 36 | else 37 | aws serverlessrepo put-application-policy --application-id ${AppArn} --statements Principals=${ShareWith},Actions=Deploy 38 | fi 39 | 40 | post_build: 41 | commands: 42 | # Copy in the executable 43 | - cp ${CODEBUILD_SRC_DIR_Built}/dist/ssosync_linux_amd64_v1/ssosync ./ 44 | 45 | # Copy in the tests 46 | - cp -r cicd/tests ./ 47 | 48 | # Copy in the stack and params templates 49 | - mkdir deploy 50 | - cp cicd/staging/build/stack.yml ./deploy/ 51 | 52 | # Update params with the values for this run for a developer account 53 | - | 54 | jq -n \ 55 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:AWS*\"}" \ 56 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 57 | '$ARGS.named' > ./deploy/developer.json 58 | - cat ./deploy/developer.json 59 | 60 | # Update params with the values for this run for the management account 61 | - | 62 | jq -n \ 63 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:Man*\"}" \ 64 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 65 | '$ARGS.named' > ./deploy/management.json 66 | - cat ./deploy/management.json 67 | 68 | # Update params with the values for this run for the delegated account 69 | - | 70 | jq -n \ 71 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:Del*\"}" \ 72 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 73 | '$ARGS.named' > ./deploy/delegated.json 74 | - cat ./deploy/delegated.json 75 | 76 | # Update params with the values for this run for non-delegated account 77 | - | 78 | jq -n \ 79 | --argjson Parameters "{\"AppArn\": \"$AppArn\", \"AppVersion\": \"$AppVersion\", \"GoogleAdminEmailArn\": \"$SecretGoogleAdminEmail\", \"GoogleCredentialsArn\": \"$SecretGoogleCredentials\", \"SCIMEndpointUrlArn\": \"$SecretSCIMEndpoint\", \"SCIMAccessTokenArn\": \"$SecretSCIMAccessToken\", \"RegionArn\": \"$SecretRegion\", \"IdentityStoreIdArn\": \"$SecretIdentityStoreID\", \"GroupMatch\": \"name:Non*\"}" \ 80 | --argjson StackPolicy "{\"Statement\":[{\"Effect\": \"Allow\", \"NotAction\": \"Update:Delete\", \"Principal\": \"*\", \"Resource\": \"*\"}]}" \ 81 | '$ARGS.named' > ./deploy/nondelegated.json 82 | - cat ./deploy/nondelegated.json 83 | 84 | 85 | artifacts: 86 | files: 87 | - ssosync 88 | - deploy/**/* 89 | - tests/**/* 90 | -------------------------------------------------------------------------------- /cicd/deploy_patterns/staging/params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "AppArn": "APPARN", 4 | "AppVersion": "APPVERSION" 5 | }, 6 | "StackPolicy": { 7 | "Statement": [{ 8 | "Effect": "Allow", 9 | "NotAction": "Update:Delete", 10 | "Principal": "*", 11 | "Resource": "*" 12 | }] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cicd/deploy_patterns/staging/stack.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Description: 5 | This CloudFormation template will deploy an instance of the SSOSync-Staging 6 | candidate releases (via privately shared app in the AWS Serverless Application 7 | Repository (SAR) within the Staging Account. 8 | 9 | Parameters: 10 | AppArn: 11 | Description: The candidate release in the SAR 12 | Default: 'arn:aws:serverlessrepo:::applications/' 13 | Type: String 14 | AppVersion: 15 | Description: The version of this build in SAR 16 | Default: 'v1.0.0-rc.10' 17 | Type: String 18 | GoogleAdminEmailArn: 19 | Type: String 20 | GoogleCredentialsArn: 21 | Type: String 22 | SCIMEndpointUrlArn: 23 | Type: String 24 | SCIMAccessTokenArn: 25 | Type: String 26 | RegionArn: 27 | Type: String 28 | IdentityStoreIdArn: 29 | Type: String 30 | GroupMatch: 31 | Description: The search string to match Groups in Google Workspace 32 | Default: 'name:AWS*,name=AWS_TEST' 33 | Type: String 34 | 35 | Resources: 36 | SARApp: 37 | Type: AWS::Serverless::Application 38 | Properties: 39 | Location: 40 | ApplicationId: !Ref AppArn 41 | SemanticVersion: !Ref AppVersion 42 | Parameters: 43 | FunctionName: SSOSyncFunction 44 | GoogleAdminEmail: !Join 45 | - '' 46 | - - '{{resolve:secretsmanager:' 47 | - !Ref GoogleAdminEmailArn 48 | - '}}' 49 | GoogleCredentials: !Join 50 | - '' 51 | - - '{{resolve:secretsmanager:' 52 | - !Ref GoogleCredentialsArn 53 | - '}}' 54 | SCIMEndpointUrl: !Join 55 | - '' 56 | - - '{{resolve:secretsmanager:' 57 | - !Ref SCIMEndpointUrlArn 58 | - '}}' 59 | SCIMEndpointAccessToken: !Join 60 | - '' 61 | - - '{{resolve:secretsmanager:' 62 | - !Ref SCIMAccessTokenArn 63 | - '}}' 64 | Region: !Join 65 | - '' 66 | - - '{{resolve:secretsmanager:' 67 | - !Ref RegionArn 68 | - '}}' 69 | IdentityStoreID: !Join 70 | - '' 71 | - - '{{resolve:secretsmanager:' 72 | - !Ref IdentityStoreIdArn 73 | - '}}' 74 | SyncMethod: groups 75 | GoogleGroupMatch: !Ref GroupMatch 76 | LogLevel: warn 77 | LogFormat: json 78 | -------------------------------------------------------------------------------- /cicd/deploy_patterns/testing/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | ShareWith: "NOT-SHARED" 6 | interval: 10 7 | Success: '"Succeeded"' 8 | InProgress: '"InProgress"' 9 | Status: '"InProgress"' 10 | 11 | phases: 12 | pre_build: 13 | commands: 14 | # Print all environment variables (handy for AWS CodeBuild logs 15 | - env 16 | 17 | build: 18 | commands: 19 | # zip up the content of TESTS 20 | - cp -r ${CODEBUILD_SRC_DIR_Tests}/* ./ 21 | - zip -r tests.zip ./ssosync 22 | - zip -r tests.zip ./tests 23 | - zip -r tests.zip ./deploy 24 | 25 | # Auth into the Staging Account 26 | - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role --role-arn "${StagingRole}" --role-session-name "CodePipelineRole" --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text)) 27 | 28 | # upload the zipfile to the S3 Bucket 29 | - aws s3 cp ./tests.zip s3://${TARGETS3BUCKET}/ 30 | 31 | # Start the test pipeline in the staging account 32 | - export ExecutionId=$(aws codepipeline start-pipeline-execution --name $pipeline --output text) 33 | - echo "ExecutionId=" $ExecutionId 34 | 35 | - | 36 | while expr "$Status" : "$InProgress" >/dev/null; do 37 | sleep $interval 38 | export Status="$(aws codepipeline get-pipeline-execution --pipeline-name $pipeline --output json --pipeline-execution-id $ExecutionId --query "pipelineExecution.status")" 39 | echo $Status 40 | done 41 | 42 | - echo "We are done" 43 | 44 | - | 45 | if expr "$Status" : "$Success" >/dev/null; then 46 | exit 0 47 | else 48 | exit 252 49 | fi 50 | 51 | -------------------------------------------------------------------------------- /cicd/release/approve/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | pre_build: 5 | commands: 6 | # Print all environment variables (handy for AWS CodeBuild logs) 7 | - env 8 | 9 | build: 10 | commands: 11 | # Check whether this is a commit on the default branch with a release tag 12 | - | 13 | if expr "${GitIsRelease}" : "true" >/dev/null; then 14 | exit 0 15 | else 16 | exit 252 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /cicd/release/public/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | # Print all environment variables (handy for AWS CodeBuild logs) 7 | - env 8 | - ls -la 9 | 10 | # Update sam to latest version 11 | - wget -q https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip 12 | - unzip -q aws-sam-cli-linux-x86_64.zip -d sam-installation 13 | - sudo ./sam-installation/install --update 14 | - rm -rf ./sam-installation aws-sam-cli-linux-x86_64.zip 15 | 16 | pre_build: 17 | commands: 18 | # Copy in the package file 19 | - cp ${CODEBUILD_SRC_DIR_Packaged}/packaged-release.yaml ./packaged.yaml 20 | 21 | # Check we have the required file 22 | - ls packaged.yaml 23 | 24 | - export AppVersion="${GitTag#v}" 25 | - echo ${AppVersion} 26 | 27 | build: 28 | commands: 29 | # Package our application with AWS SAM 30 | - sam publish --template packaged.yaml --semantic-version ${AppVersion} 31 | -------------------------------------------------------------------------------- /cicd/tests/account_execution/cli/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | secrets-manager: 5 | GoogleAdminEmail: SSOSyncGoogleAdminEmail 6 | SCIMEndpointUrl: SSOSyncSCIMEndpointUrl 7 | SCIMAccessToken: SSOSyncSCIMAccessToken 8 | IdentityStoreID: SSOSyncIdentityStoreID 9 | Region: SSOSyncRegion 10 | variables: 11 | ExpectedExitState: 0 12 | 13 | phases: 14 | pre_build: 15 | commands: 16 | # Print all environment variables (handy for AWS CodeBuild logs) 17 | - env 18 | 19 | build: 20 | commands: 21 | - ./ssosync --version 22 | - aws secretsmanager get-secret-value --secret-id=SSOSyncGoogleCredentials --query SecretString --output text | jq '.' > credentials.json 23 | - cat credentials.json 24 | 25 | - ./ssosync -t "${SCIMAccessToken}" -e "${SCIMEndpointUrl}" -u "${GoogleAdminEmail}" -i "${IdentityStoreID}" -r "${Region}" -s "groups" -g "name:AWS*"; ExitState=$? 26 | 27 | - | 28 | if expr "${ExitState}" : "${ExpectedExitState}" >/dev/null; then 29 | echo "We got what we expected" 30 | exit 0 31 | else 32 | echo "We didn't get what we expected" 33 | exit 1 34 | fi 35 | 36 | -------------------------------------------------------------------------------- /cicd/tests/account_execution/lambda/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | ExpectedResponse: "false" 6 | 7 | phases: 8 | pre_build: 9 | commands: 10 | # Print all environment variables (handy for AWS CodeBuild logs) 11 | - env 12 | 13 | build: 14 | commands: 15 | # Execute the lambda 16 | - FunctionError=$(aws lambda invoke --function-name "SSOSyncFunction" response.json | jq 'has("FunctionError")') 17 | 18 | - | 19 | if expr "${FunctionError}" : "${ExpectedResponse}" >/dev/null; then 20 | echo "We got what we expected" 21 | exit 0 22 | else 23 | echo "We didn't get what we expected" 24 | exit 1 25 | fi 26 | 27 | 28 | artifacts: 29 | files: 30 | - response.json 31 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cmd ... 16 | package cmd 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | "strings" 23 | 24 | "github.com/aws/aws-lambda-go/events" 25 | "github.com/aws/aws-lambda-go/lambda" 26 | "github.com/aws/aws-sdk-go/aws" 27 | "github.com/aws/aws-sdk-go/aws/session" 28 | "github.com/aws/aws-sdk-go/service/codepipeline" 29 | "github.com/aws/aws-sdk-go/service/secretsmanager" 30 | "github.com/awslabs/ssosync/internal" 31 | "github.com/awslabs/ssosync/internal/config" 32 | "github.com/pkg/errors" 33 | log "github.com/sirupsen/logrus" 34 | "github.com/spf13/cobra" 35 | "github.com/spf13/viper" 36 | ) 37 | 38 | var ( 39 | version = "dev" 40 | commit = "none" 41 | date = "unknown" 42 | builtBy = "unknown" 43 | ) 44 | 45 | var cfg *config.Config 46 | 47 | var rootCmd = &cobra.Command{ 48 | Version: "dev", 49 | Use: "ssosync", 50 | Short: "SSO Sync, making AWS SSO be populated automagically", 51 | Long: `A command line tool to enable you to synchronise your Google 52 | Apps (Google Workspace) users to AWS Single Sign-on (AWS SSO) 53 | Complete documentation is available at https://github.com/awslabs/ssosync`, 54 | RunE: func(cmd *cobra.Command, args []string) error { 55 | ctx, cancel := context.WithCancel(context.Background()) 56 | defer cancel() 57 | 58 | err := internal.DoSync(ctx, cfg) 59 | if err != nil { 60 | return err 61 | } 62 | return nil 63 | }, 64 | } 65 | 66 | // Execute is the entry point of the command. If we are 67 | // running inside of AWS Lambda, we use the Lambda 68 | // execution path. 69 | func Execute() { 70 | if cfg.IsLambda { 71 | log.Info("Executing as Lambda") 72 | lambda.Start(Handler) 73 | } 74 | 75 | if err := rootCmd.Execute(); err != nil { 76 | log.Fatal(err) 77 | } 78 | } 79 | 80 | // Handler for when executing as a lambda 81 | func Handler(ctx context.Context, event events.CodePipelineEvent) (string, error) { 82 | log.Debug(event) 83 | err := rootCmd.Execute() 84 | s := session.Must(session.NewSession()) 85 | cpl := codepipeline.New(s) 86 | 87 | cfg.IsLambdaRunningInCodePipeline = len(event.CodePipelineJob.ID) > 0 88 | 89 | if cfg.IsLambdaRunningInCodePipeline { 90 | log.Info("Lambda has been invoked by CodePipeline") 91 | 92 | if err != nil { 93 | // notify codepipeline and mark its job execution as Failure 94 | log.Fatalf(errors.Wrap(err, "Notifying CodePipeline and mark its job execution as Failure").Error()) 95 | jobID := event.CodePipelineJob.ID 96 | if len(jobID) == 0 { 97 | panic("CodePipeline Job ID is not set") 98 | } 99 | // mark the job as Failure. 100 | cplFailure := &codepipeline.PutJobFailureResultInput{ 101 | JobId: aws.String(jobID), 102 | FailureDetails: &codepipeline.FailureDetails{ 103 | Message: aws.String(err.Error()), 104 | Type: aws.String("JobFailed"), 105 | }, 106 | } 107 | _, cplErr := cpl.PutJobFailureResult(cplFailure) 108 | if cplErr != nil { 109 | log.Fatalf(errors.Wrap(err, "Failed to update CodePipeline jobID status").Error()) 110 | } 111 | return "Failure", err 112 | } 113 | 114 | log.Info("Notifying CodePipeline and mark its job execution as Success") 115 | jobID := event.CodePipelineJob.ID 116 | if len(jobID) == 0 { 117 | panic("CodePipeline Job ID is not set") 118 | } 119 | // mark the job as Success. 120 | cplSuccess := &codepipeline.PutJobSuccessResultInput{ 121 | JobId: aws.String(jobID), 122 | } 123 | _, cplErr := cpl.PutJobSuccessResult(cplSuccess) 124 | if cplErr != nil { 125 | log.Fatalf(errors.Wrap(err, "Failed to update CodePipeline jobID status").Error()) 126 | } 127 | 128 | return "Success", nil 129 | } 130 | 131 | if err != nil { 132 | log.Fatalf(errors.Wrap(err, "Notifying Lambda and mark this execution as Failure").Error()) 133 | return "Failure", err 134 | } 135 | return "Success", nil 136 | } 137 | 138 | func init() { 139 | // init config 140 | cfg = config.New() 141 | cfg.IsLambda = len(os.Getenv("AWS_LAMBDA_FUNCTION_NAME")) > 0 142 | 143 | // initialize cobra 144 | cobra.OnInitialize(initConfig) 145 | addFlags(rootCmd, cfg) 146 | 147 | rootCmd.SetVersionTemplate(fmt.Sprintf("%s, commit %s, built at %s by %s\n", version, commit, date, builtBy)) 148 | 149 | // silence on the root cmd 150 | rootCmd.SilenceUsage = true 151 | rootCmd.SilenceErrors = true 152 | } 153 | 154 | // initConfig reads in config file and ENV variables if set. 155 | func initConfig() { 156 | // allow to read in from environment 157 | viper.SetEnvPrefix("ssosync") 158 | viper.AutomaticEnv() 159 | 160 | appEnvVars := []string{ 161 | "google_admin", 162 | "google_credentials", 163 | "scim_access_token", 164 | "scim_endpoint", 165 | "log_level", 166 | "log_format", 167 | "ignore_users", 168 | "ignore_groups", 169 | "include_groups", 170 | "user_match", 171 | "group_match", 172 | "sync_method", 173 | "region", 174 | "identity_store_id", 175 | } 176 | 177 | for _, e := range appEnvVars { 178 | if err := viper.BindEnv(e); err != nil { 179 | log.Fatalf(errors.Wrap(err, "cannot bind environment variable").Error()) 180 | } 181 | } 182 | 183 | if err := viper.Unmarshal(&cfg); err != nil { 184 | log.Fatalf(errors.Wrap(err, "cannot unmarshal config").Error()) 185 | } 186 | 187 | if cfg.IsLambda { 188 | configLambda() 189 | } 190 | 191 | // config logger 192 | logConfig(cfg) 193 | 194 | } 195 | 196 | func configLambda() { 197 | s := session.Must(session.NewSession()) 198 | svc := secretsmanager.New(s) 199 | secrets := config.NewSecrets(svc) 200 | 201 | unwrap, err := secrets.GoogleAdminEmail(os.Getenv("GOOGLE_ADMIN")) 202 | if err != nil { 203 | log.Fatalf(errors.Wrap(err, "cannot read config: GOOGLE_ADMIN").Error()) 204 | } 205 | cfg.GoogleAdmin = unwrap 206 | 207 | unwrap, err = secrets.GoogleCredentials(os.Getenv("GOOGLE_CREDENTIALS")) 208 | if err != nil { 209 | log.Fatalf(errors.Wrap(err, "cannot read config: GOOGLE_CREDENTIALS").Error()) 210 | } 211 | cfg.GoogleCredentials = unwrap 212 | 213 | unwrap, err = secrets.SCIMAccessToken(os.Getenv("SCIM_ACCESS_TOKEN")) 214 | if err != nil { 215 | log.Fatalf(errors.Wrap(err, "cannot read config: SCIM_ACCESS_TOKEN").Error()) 216 | } 217 | cfg.SCIMAccessToken = unwrap 218 | 219 | unwrap, err = secrets.SCIMEndpointURL(os.Getenv("SCIM_ENDPOINT")) 220 | if err != nil { 221 | log.Fatalf(errors.Wrap(err, "cannot read config: SCIM_ENDPOINT").Error()) 222 | } 223 | cfg.SCIMEndpoint = unwrap 224 | 225 | unwrap, err = secrets.Region(os.Getenv("REGION")) 226 | if err != nil { 227 | log.Fatalf(errors.Wrap(err, "cannot read config: REGION").Error()) 228 | } 229 | cfg.Region = unwrap 230 | 231 | unwrap, err = secrets.IdentityStoreID(os.Getenv("IDENTITY_STORE_ID")) 232 | if err != nil { 233 | log.Fatalf(errors.Wrap(err, "cannot read config: IDENTITY_STORE_ID").Error()) 234 | } 235 | cfg.IdentityStoreID = unwrap 236 | 237 | unwrap = os.Getenv("LOG_LEVEL") 238 | if len([]rune(unwrap)) != 0 { 239 | cfg.LogLevel = unwrap 240 | log.WithField("LogLevel", unwrap).Debug("from EnvVar") 241 | } 242 | 243 | unwrap = os.Getenv("LOG_FORMAT") 244 | if len([]rune(unwrap)) != 0 { 245 | cfg.LogFormat = unwrap 246 | log.WithField("LogFormay", unwrap).Debug("from EnvVar") 247 | } 248 | 249 | unwrap = os.Getenv("SYNC_METHOD") 250 | if len([]rune(unwrap)) != 0 { 251 | cfg.SyncMethod = unwrap 252 | log.WithField("SyncMethod", unwrap).Debug("from EnvVar") 253 | } 254 | 255 | unwrap = os.Getenv("USER_MATCH") 256 | if len([]rune(unwrap)) != 0 { 257 | cfg.UserMatch = unwrap 258 | log.WithField("UserMatch", unwrap).Debug("from EnvVar") 259 | } 260 | 261 | unwrap = os.Getenv("GROUP_MATCH") 262 | if len([]rune(unwrap)) != 0 { 263 | cfg.GroupMatch = unwrap 264 | log.WithField("GroupMatch", unwrap).Debug("from EnvVar") 265 | } 266 | 267 | unwrap = os.Getenv("IGNORE_GROUPS") 268 | if len([]rune(unwrap)) != 0 { 269 | cfg.IgnoreGroups = strings.Split(unwrap, ",") 270 | log.WithField("IgnoreGroups", unwrap).Debug("from EnvVar") 271 | } 272 | 273 | unwrap = os.Getenv("IGNORE_USERS") 274 | if len([]rune(unwrap)) != 0 { 275 | cfg.IgnoreUsers = strings.Split(unwrap, ",") 276 | log.WithField("IgnoreUsers", unwrap).Debug("from EnvVar") 277 | } 278 | 279 | unwrap = os.Getenv("INCLUDE_GROUPS") 280 | if len([]rune(unwrap)) != 0 { 281 | cfg.IncludeGroups = strings.Split(unwrap, ",") 282 | log.WithField("IncludeGroups", unwrap).Debug("from EnvVar") 283 | } 284 | 285 | } 286 | 287 | func addFlags(cmd *cobra.Command, cfg *config.Config) { 288 | rootCmd.PersistentFlags().StringVarP(&cfg.GoogleCredentials, "google-admin", "a", config.DefaultGoogleCredentials, "path to find credentials file for Google Workspace") 289 | rootCmd.PersistentFlags().BoolVarP(&cfg.Debug, "debug", "d", config.DefaultDebug, "enable verbose / debug logging") 290 | rootCmd.PersistentFlags().StringVarP(&cfg.LogFormat, "log-format", "", config.DefaultLogFormat, "log format") 291 | rootCmd.PersistentFlags().StringVarP(&cfg.LogLevel, "log-level", "", config.DefaultLogLevel, "log level") 292 | rootCmd.Flags().StringVarP(&cfg.SCIMAccessToken, "access-token", "t", "", "AWS SSO SCIM API Access Token") 293 | rootCmd.Flags().StringVarP(&cfg.SCIMEndpoint, "endpoint", "e", "", "AWS SSO SCIM API Endpoint") 294 | rootCmd.Flags().StringVarP(&cfg.GoogleCredentials, "google-credentials", "c", config.DefaultGoogleCredentials, "path to Google Workspace credentials file") 295 | rootCmd.Flags().StringVarP(&cfg.GoogleAdmin, "google-admin", "u", "", "Google Workspace admin user email") 296 | rootCmd.Flags().StringSliceVar(&cfg.IgnoreUsers, "ignore-users", []string{}, "ignores these Google Workspace users") 297 | rootCmd.Flags().StringSliceVar(&cfg.IgnoreGroups, "ignore-groups", []string{}, "ignores these Google Workspace groups") 298 | rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", []string{}, "include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups'") 299 | rootCmd.Flags().StringVarP(&cfg.UserMatch, "user-match", "m", "", "Google Workspace Users filter query parameter, example: 'name:John*' 'name=John Doe,email:admin*', to sync all users in the directory specify '*'. For query syntax and more examples see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users") 300 | rootCmd.Flags().StringVarP(&cfg.GroupMatch, "group-match", "g", "*", "Google Workspace Groups filter query parameter, example: 'name:Admin*' 'name=Admins,email:aws-*', to sync all groups (and their member users) specify '*'. For query syntax and more examples see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups") 301 | rootCmd.Flags().StringVarP(&cfg.SyncMethod, "sync-method", "s", config.DefaultSyncMethod, "Sync method to use (users_groups|groups)") 302 | rootCmd.Flags().StringVarP(&cfg.Region, "region", "r", "", "AWS Region where AWS SSO is enabled") 303 | rootCmd.Flags().StringVarP(&cfg.IdentityStoreID, "identity-store-id", "i", "", "Identifier of Identity Store in AWS SSO") 304 | } 305 | 306 | func logConfig(cfg *config.Config) { 307 | // reset log format 308 | if cfg.LogFormat == "json" { 309 | log.SetFormatter(&log.JSONFormatter{}) 310 | } 311 | 312 | if cfg.Debug { 313 | cfg.LogLevel = "debug" 314 | } 315 | 316 | // set the configured log level 317 | if level, err := log.ParseLevel(cfg.LogLevel); err == nil { 318 | log.SetLevel(level) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/ssosync 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.0.0 7 | github.com/aws/aws-lambda-go v1.23.0 8 | github.com/aws/aws-sdk-go v1.44.102 9 | github.com/fsnotify/fsnotify v1.4.9 // indirect 10 | github.com/golang/mock v1.5.0 11 | github.com/hashicorp/go-retryablehttp v0.7.7 12 | github.com/magiconair/properties v1.8.5 // indirect 13 | github.com/mitchellh/mapstructure v1.4.1 // indirect 14 | github.com/pelletier/go-toml v1.9.0 // indirect 15 | github.com/pkg/errors v0.9.1 16 | github.com/sirupsen/logrus v1.8.1 17 | github.com/spf13/afero v1.6.0 // indirect 18 | github.com/spf13/cast v1.3.1 // indirect 19 | github.com/spf13/cobra v1.1.3 20 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 21 | github.com/spf13/viper v1.7.1 22 | github.com/stretchr/testify v1.7.2 23 | golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c 24 | google.golang.org/api v0.46.0 25 | gopkg.in/ini.v1 v1.62.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /internal/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/ssosync/beff54f81747f1b1c31553f161d885b00683b3d5/internal/.DS_Store -------------------------------------------------------------------------------- /internal/aws/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "io/ioutil" 23 | "net/http" 24 | "net/url" 25 | "path" 26 | 27 | log "github.com/sirupsen/logrus" 28 | ) 29 | 30 | var ( 31 | // ErrUserNotFound 32 | ErrUserNotFound = errors.New("user not found") 33 | // ErrGroupNotFound 34 | ErrGroupNotFound = errors.New("group not found") 35 | // ErrUserNotSpecified 36 | ErrUserNotSpecified = errors.New("user not specified") 37 | ) 38 | 39 | // ErrHTTPNotOK 40 | type ErrHTTPNotOK struct { 41 | StatusCode int 42 | } 43 | 44 | func (e *ErrHTTPNotOK) Error() string { 45 | return fmt.Sprintf("status of http response was %d", e.StatusCode) 46 | } 47 | 48 | // OperationType handle patch operations for add/remove 49 | type OperationType string 50 | 51 | const ( 52 | // OperationAdd is the add operation for a patch 53 | OperationAdd OperationType = "add" 54 | 55 | // OperationRemove is the remove operation for a patch 56 | OperationRemove = "remove" 57 | ) 58 | 59 | // Client represents an interface of methods used 60 | // to communicate with AWS SSO 61 | type Client interface { 62 | CreateUser(*User) (*User, error) 63 | FindGroupByDisplayName(string) (*Group, error) 64 | FindUserByEmail(string) (*User, error) 65 | UpdateUser(*User) (*User, error) 66 | } 67 | 68 | type client struct { 69 | httpClient HTTPClient 70 | endpointURL *url.URL 71 | bearerToken string 72 | } 73 | 74 | // NewClient creates a new client to talk with AWS SSO's SCIM endpoint. It 75 | // requires a http.Client{} as well as the URL and bearer token from the 76 | // console. If the URL is not parsable, an error will be thrown. 77 | func NewClient(c HTTPClient, config *Config) (Client, error) { 78 | u, err := url.Parse(config.Endpoint) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return &client{ 83 | httpClient: c, 84 | endpointURL: u, 85 | bearerToken: config.Token, 86 | }, nil 87 | } 88 | 89 | // sendRequestWithBody will send the body given to the url/method combination 90 | // with the right Bearer token as well as the correct content type for SCIM. 91 | func (c *client) sendRequestWithBody(method string, url string, body interface{}) (response []byte, err error) { 92 | // Convert the body to JSON 93 | d, err := json.Marshal(body) 94 | if err != nil { 95 | return 96 | } 97 | 98 | // Create a request with our body of JSON 99 | r, err := http.NewRequest(method, url, bytes.NewBuffer(d)) 100 | if err != nil { 101 | return 102 | } 103 | 104 | log.WithFields(log.Fields{"url": url, "method": method}) 105 | 106 | // Set the content-type and authorization headers 107 | r.Header.Set("Content-Type", "application/scim+json") 108 | r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken)) 109 | 110 | // Call the URL 111 | resp, err := c.httpClient.Do(r) 112 | if err != nil { 113 | return 114 | } 115 | defer resp.Body.Close() 116 | 117 | // Read the body back from the response 118 | response, err = ioutil.ReadAll(resp.Body) 119 | if err != nil { 120 | return 121 | } 122 | 123 | // If we get a non-2xx status code, raise that via an error 124 | if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent { 125 | err = &ErrHTTPNotOK{resp.StatusCode} 126 | } 127 | 128 | return 129 | } 130 | 131 | func (c *client) sendRequest(method string, url string) (response []byte, err error) { 132 | r, err := http.NewRequest(method, url, nil) 133 | if err != nil { 134 | return 135 | } 136 | 137 | log.WithFields(log.Fields{"url": url, "method": method}) 138 | 139 | r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken)) 140 | 141 | resp, err := c.httpClient.Do(r) 142 | if err != nil { 143 | return 144 | } 145 | 146 | defer resp.Body.Close() 147 | response, err = ioutil.ReadAll(resp.Body) 148 | if err != nil { 149 | return 150 | } 151 | 152 | if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent { 153 | err = fmt.Errorf("status of http response was %d", resp.StatusCode) 154 | } 155 | 156 | return 157 | } 158 | 159 | // FindUserByEmail will find the user by the email address specified 160 | func (c *client) FindUserByEmail(email string) (*User, error) { 161 | startURL, err := url.Parse(c.endpointURL.String()) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | filter := fmt.Sprintf("userName eq \"%s\"", email) 167 | 168 | startURL.Path = path.Join(startURL.Path, "/Users") 169 | q := startURL.Query() 170 | q.Add("filter", filter) 171 | 172 | startURL.RawQuery = q.Encode() 173 | 174 | resp, err := c.sendRequest(http.MethodGet, startURL.String()) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | var r UserFilterResults 180 | err = json.Unmarshal(resp, &r) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | if r.TotalResults != 1 { 186 | return nil, ErrUserNotFound 187 | } 188 | 189 | return &r.Resources[0], nil 190 | } 191 | 192 | // FindGroupByDisplayName will find the group by its displayname. 193 | func (c *client) FindGroupByDisplayName(name string) (*Group, error) { 194 | startURL, err := url.Parse(c.endpointURL.String()) 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | filter := fmt.Sprintf("displayName eq \"%s\"", name) 200 | 201 | startURL.Path = path.Join(startURL.Path, "/Groups") 202 | q := startURL.Query() 203 | q.Add("filter", filter) 204 | 205 | startURL.RawQuery = q.Encode() 206 | 207 | resp, err := c.sendRequest(http.MethodGet, startURL.String()) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | var r GroupFilterResults 213 | err = json.Unmarshal(resp, &r) 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | if r.TotalResults != 1 { 219 | return nil, ErrGroupNotFound 220 | } 221 | 222 | return &r.Resources[0], nil 223 | } 224 | 225 | // CreateUser will create the user specified 226 | func (c *client) CreateUser(u *User) (*User, error) { 227 | startURL, err := url.Parse(c.endpointURL.String()) 228 | if err != nil { 229 | return nil, err 230 | } 231 | 232 | if u == nil { 233 | err = ErrUserNotSpecified 234 | return nil, err 235 | } 236 | 237 | startURL.Path = path.Join(startURL.Path, "/Users") 238 | resp, err := c.sendRequestWithBody(http.MethodPost, startURL.String(), *u) 239 | if err != nil { 240 | return nil, err 241 | } 242 | 243 | var newUser User 244 | err = json.Unmarshal(resp, &newUser) 245 | if err != nil { 246 | return nil, err 247 | } 248 | if newUser.ID == "" { 249 | return c.FindUserByEmail(u.Username) 250 | } 251 | 252 | return &newUser, nil 253 | } 254 | 255 | // UpdateUser will update/replace the user specified 256 | func (c *client) UpdateUser(u *User) (*User, error) { 257 | startURL, err := url.Parse(c.endpointURL.String()) 258 | if err != nil { 259 | return nil, err 260 | } 261 | 262 | if u == nil { 263 | err = ErrUserNotFound 264 | return nil, err 265 | } 266 | 267 | startURL.Path = path.Join(startURL.Path, fmt.Sprintf("/Users/%s", u.ID)) 268 | resp, err := c.sendRequestWithBody(http.MethodPut, startURL.String(), *u) 269 | if err != nil { 270 | return nil, err 271 | } 272 | 273 | var newUser User 274 | err = json.Unmarshal(resp, &newUser) 275 | if err != nil { 276 | return nil, err 277 | } 278 | if newUser.ID == "" { 279 | return c.FindUserByEmail(u.Username) 280 | } 281 | 282 | return &newUser, nil 283 | } 284 | -------------------------------------------------------------------------------- /internal/aws/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "io/ioutil" 23 | "net/http" 24 | "net/url" 25 | "testing" 26 | 27 | "github.com/golang/mock/gomock" 28 | "github.com/stretchr/testify/assert" 29 | 30 | "github.com/awslabs/ssosync/internal/aws/mock" 31 | ) 32 | 33 | type nopCloser struct { 34 | io.Reader 35 | } 36 | 37 | func (nopCloser) Close() error { return nil } 38 | 39 | type httpReqMatcher struct { 40 | httpReq *http.Request 41 | headers map[string]string 42 | body string 43 | } 44 | 45 | func (r *httpReqMatcher) Matches(req interface{}) bool { 46 | m, ok := req.(*http.Request) 47 | if !ok { 48 | return false 49 | } 50 | 51 | for k, v := range r.headers { 52 | if m.Header.Get(k) != v { 53 | return false 54 | } 55 | } 56 | 57 | if m.Body != nil { 58 | got, _ := ioutil.ReadAll(m.Body) 59 | if string(got) != r.body { 60 | return false 61 | } 62 | } 63 | 64 | return m.URL.String() == r.httpReq.URL.String() && m.Method == r.httpReq.Method 65 | } 66 | 67 | func (r *httpReqMatcher) String() string { 68 | return fmt.Sprintf("is %s", r.httpReq.URL) 69 | } 70 | 71 | func TestNewClient(t *testing.T) { 72 | ctrl := gomock.NewController(t) 73 | defer ctrl.Finish() 74 | 75 | x := mock.NewIHTTPClient(ctrl) 76 | 77 | c, err := NewClient(x, &Config{ 78 | Endpoint: ":foo", 79 | Token: "bearerToken", 80 | }) 81 | assert.Error(t, err) 82 | assert.Nil(t, c) 83 | } 84 | 85 | func TestSendRequestBadUrl(t *testing.T) { 86 | ctrl := gomock.NewController(t) 87 | defer ctrl.Finish() 88 | 89 | x := mock.NewIHTTPClient(ctrl) 90 | 91 | c, err := NewClient(x, &Config{ 92 | Endpoint: "https://scim.example.com/", 93 | Token: "bearerToken", 94 | }) 95 | assert.NoError(t, err) 96 | cc := c.(*client) 97 | 98 | r, err := cc.sendRequest(http.MethodGet, ":foo") 99 | assert.Error(t, err) 100 | assert.Nil(t, r) 101 | } 102 | 103 | func TestSendRequestBadStatusCode(t *testing.T) { 104 | ctrl := gomock.NewController(t) 105 | defer ctrl.Finish() 106 | 107 | x := mock.NewIHTTPClient(ctrl) 108 | 109 | c, err := NewClient(x, &Config{ 110 | Endpoint: "https://scim.example.com/", 111 | Token: "bearerToken", 112 | }) 113 | assert.NoError(t, err) 114 | cc := c.(*client) 115 | 116 | calledURL, _ := url.Parse("https://scim.example.com/") 117 | 118 | req := httpReqMatcher{httpReq: &http.Request{ 119 | URL: calledURL, 120 | Method: http.MethodGet, 121 | }} 122 | 123 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 124 | Status: "ERROR", 125 | StatusCode: 500, 126 | Body: nopCloser{bytes.NewBufferString("")}, 127 | }, nil) 128 | 129 | _, err = cc.sendRequest(http.MethodGet, "https://scim.example.com/") 130 | assert.Error(t, err) 131 | } 132 | 133 | func TestSendRequestCheckAuthHeader(t *testing.T) { 134 | ctrl := gomock.NewController(t) 135 | defer ctrl.Finish() 136 | 137 | x := mock.NewIHTTPClient(ctrl) 138 | 139 | c, err := NewClient(x, &Config{ 140 | Endpoint: "https://scim.example.com/", 141 | Token: "bearerToken", 142 | }) 143 | assert.NoError(t, err) 144 | cc := c.(*client) 145 | 146 | calledURL, _ := url.Parse("https://scim.example.com/") 147 | 148 | req := httpReqMatcher{ 149 | httpReq: &http.Request{ 150 | URL: calledURL, 151 | Method: http.MethodGet, 152 | }, 153 | headers: map[string]string{ 154 | "Authorization": "Bearer bearerToken", 155 | }, 156 | } 157 | 158 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 159 | Status: "OK", 160 | StatusCode: 200, 161 | Body: nopCloser{bytes.NewBufferString("")}, 162 | }, nil) 163 | 164 | _, err = cc.sendRequest(http.MethodGet, "https://scim.example.com/") 165 | assert.NoError(t, err) 166 | } 167 | 168 | func TestSendRequestWithBodyCheckHeaders(t *testing.T) { 169 | ctrl := gomock.NewController(t) 170 | defer ctrl.Finish() 171 | 172 | x := mock.NewIHTTPClient(ctrl) 173 | 174 | c, err := NewClient(x, &Config{ 175 | Endpoint: "https://scim.example.com/", 176 | Token: "bearerToken", 177 | }) 178 | assert.NoError(t, err) 179 | cc := c.(*client) 180 | 181 | calledURL, _ := url.Parse("https://scim.example.com/") 182 | 183 | req := httpReqMatcher{ 184 | httpReq: &http.Request{ 185 | URL: calledURL, 186 | Method: http.MethodPost, 187 | }, 188 | headers: map[string]string{ 189 | "Authorization": "Bearer bearerToken", 190 | "Content-Type": "application/scim+json", 191 | }, 192 | body: "{\"schemas\":null,\"userName\":\"\",\"name\":{\"familyName\":\"\",\"givenName\":\"\"},\"displayName\":\"\",\"active\":false,\"emails\":null,\"addresses\":null}", 193 | } 194 | 195 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 196 | Status: "OK", 197 | StatusCode: 200, 198 | Body: nopCloser{bytes.NewBufferString("")}, 199 | }, nil) 200 | 201 | _, err = cc.sendRequestWithBody(http.MethodPost, "https://scim.example.com/", &User{}) 202 | assert.NoError(t, err) 203 | } 204 | 205 | func TestClient_FindUserByEmail(t *testing.T) { 206 | ctrl := gomock.NewController(t) 207 | defer ctrl.Finish() 208 | 209 | x := mock.NewIHTTPClient(ctrl) 210 | 211 | c, err := NewClient(x, &Config{ 212 | Endpoint: "https://scim.example.com/", 213 | Token: "bearerToken", 214 | }) 215 | assert.NoError(t, err) 216 | 217 | calledURL, _ := url.Parse("https://scim.example.com/Users") 218 | 219 | filter := "userName eq \"test@example.com\"" 220 | 221 | q := calledURL.Query() 222 | q.Add("filter", filter) 223 | 224 | calledURL.RawQuery = q.Encode() 225 | 226 | req := httpReqMatcher{ 227 | httpReq: &http.Request{ 228 | URL: calledURL, 229 | Method: http.MethodGet, 230 | }, 231 | } 232 | 233 | // Error in response 234 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 235 | Status: "OK", 236 | StatusCode: 200, 237 | Body: nopCloser{bytes.NewBufferString("")}, 238 | }, nil) 239 | 240 | u, err := c.FindUserByEmail("test@example.com") 241 | assert.Nil(t, u) 242 | assert.Error(t, err) 243 | 244 | // False 245 | r := &UserFilterResults{ 246 | TotalResults: 0, 247 | } 248 | falseResult, _ := json.Marshal(r) 249 | 250 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 251 | Status: "OK", 252 | StatusCode: 200, 253 | Body: nopCloser{bytes.NewBuffer(falseResult)}, 254 | }, nil) 255 | 256 | u, err = c.FindUserByEmail("test@example.com") 257 | assert.Nil(t, u) 258 | assert.Error(t, err) 259 | 260 | // True 261 | r = &UserFilterResults{ 262 | TotalResults: 1, 263 | Resources: []User{ 264 | { 265 | Username: "test@example.com", 266 | }, 267 | }, 268 | } 269 | trueResult, _ := json.Marshal(r) 270 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 271 | Status: "OK", 272 | StatusCode: 200, 273 | Body: nopCloser{bytes.NewBuffer(trueResult)}, 274 | }, nil) 275 | 276 | u, err = c.FindUserByEmail("test@example.com") 277 | assert.NotNil(t, u) 278 | assert.NoError(t, err) 279 | } 280 | 281 | func TestClient_FindGroupByDisplayName(t *testing.T) { 282 | ctrl := gomock.NewController(t) 283 | defer ctrl.Finish() 284 | 285 | x := mock.NewIHTTPClient(ctrl) 286 | 287 | c, err := NewClient(x, &Config{ 288 | Endpoint: "https://scim.example.com/", 289 | Token: "bearerToken", 290 | }) 291 | assert.NoError(t, err) 292 | 293 | calledURL, _ := url.Parse("https://scim.example.com/Groups") 294 | 295 | filter := "displayName eq \"testGroup\"" 296 | 297 | q := calledURL.Query() 298 | q.Add("filter", filter) 299 | 300 | calledURL.RawQuery = q.Encode() 301 | 302 | req := httpReqMatcher{ 303 | httpReq: &http.Request{ 304 | URL: calledURL, 305 | Method: http.MethodGet, 306 | }, 307 | } 308 | 309 | // Error in response 310 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 311 | Status: "OK", 312 | StatusCode: 200, 313 | Body: nopCloser{bytes.NewBufferString("")}, 314 | }, nil) 315 | 316 | u, err := c.FindGroupByDisplayName("testGroup") 317 | assert.Nil(t, u) 318 | assert.Error(t, err) 319 | 320 | // False 321 | r := &GroupFilterResults{ 322 | TotalResults: 0, 323 | } 324 | falseResult, _ := json.Marshal(r) 325 | 326 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 327 | Status: "OK", 328 | StatusCode: 200, 329 | Body: nopCloser{bytes.NewBuffer(falseResult)}, 330 | }, nil) 331 | 332 | u, err = c.FindGroupByDisplayName("testGroup") 333 | assert.Nil(t, u) 334 | assert.Error(t, err) 335 | 336 | // True 337 | r = &GroupFilterResults{ 338 | TotalResults: 1, 339 | Resources: []Group{ 340 | { 341 | DisplayName: "testGroup", 342 | }, 343 | }, 344 | } 345 | trueResult, _ := json.Marshal(r) 346 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 347 | Status: "OK", 348 | StatusCode: 200, 349 | Body: nopCloser{bytes.NewBuffer(trueResult)}, 350 | }, nil) 351 | 352 | u, err = c.FindGroupByDisplayName("testGroup") 353 | assert.NotNil(t, u) 354 | assert.NoError(t, err) 355 | } 356 | 357 | func TestClient_CreateUser(t *testing.T) { 358 | nu := NewUser("Lee", "Packham", "test@example.com", true) 359 | nuResult := *nu 360 | nuResult.ID = "userId" 361 | 362 | ctrl := gomock.NewController(t) 363 | defer ctrl.Finish() 364 | 365 | x := mock.NewIHTTPClient(ctrl) 366 | 367 | c, err := NewClient(x, &Config{ 368 | Endpoint: "https://scim.example.com/", 369 | Token: "bearerToken", 370 | }) 371 | assert.NoError(t, err) 372 | 373 | calledURL, _ := url.Parse("https://scim.example.com/Users") 374 | 375 | requestJSON, _ := json.Marshal(nu) 376 | 377 | req := httpReqMatcher{ 378 | httpReq: &http.Request{ 379 | URL: calledURL, 380 | Method: http.MethodPost, 381 | }, 382 | body: string(requestJSON), 383 | } 384 | 385 | response, _ := json.Marshal(nuResult) 386 | 387 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 388 | Status: "OK", 389 | StatusCode: 200, 390 | Body: nopCloser{bytes.NewBuffer(response)}, 391 | }, nil) 392 | 393 | r, err := c.CreateUser(nu) 394 | assert.NotNil(t, r) 395 | assert.NoError(t, err) 396 | 397 | if r != nil { 398 | assert.Equal(t, *r, nuResult) 399 | } 400 | } 401 | 402 | func TestClient_UpdateUser(t *testing.T) { 403 | nu := UpdateUser("userId", "Lee", "Packham", "test@example.com", true) 404 | nuResult := *nu 405 | nuResult.ID = "userId" 406 | 407 | ctrl := gomock.NewController(t) 408 | defer ctrl.Finish() 409 | 410 | x := mock.NewIHTTPClient(ctrl) 411 | 412 | c, err := NewClient(x, &Config{ 413 | Endpoint: "https://scim.example.com/", 414 | Token: "bearerToken", 415 | }) 416 | assert.NoError(t, err) 417 | 418 | calledURL, _ := url.Parse("https://scim.example.com/Users/userId") 419 | 420 | requestJSON, _ := json.Marshal(nu) 421 | 422 | req := httpReqMatcher{ 423 | httpReq: &http.Request{ 424 | URL: calledURL, 425 | Method: http.MethodPut, 426 | }, 427 | body: string(requestJSON), 428 | } 429 | 430 | response, _ := json.Marshal(nuResult) 431 | 432 | x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ 433 | Status: "OK", 434 | StatusCode: 200, 435 | Body: nopCloser{bytes.NewBuffer(response)}, 436 | }, nil) 437 | 438 | r, err := c.UpdateUser(nu) 439 | assert.NotNil(t, r) 440 | assert.NoError(t, err) 441 | 442 | if r != nil { 443 | assert.Equal(t, *r, nuResult) 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /internal/aws/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | import "github.com/BurntSushi/toml" 18 | 19 | // Config specifes the configuration needed for AWS SSO SCIM 20 | type Config struct { 21 | Endpoint string 22 | Token string 23 | } 24 | 25 | // ReadConfigFromFile will read a TOML file into the Config Struct 26 | func ReadConfigFromFile(path string) (*Config, error) { 27 | var c Config 28 | _, err := toml.DecodeFile(path, &c) 29 | return &c, err 30 | } 31 | -------------------------------------------------------------------------------- /internal/aws/groups.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | // NewGroup creates an object representing a group with the given name 18 | func NewGroup(groupName string) *Group { 19 | return &Group{ 20 | Schemas: []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, 21 | DisplayName: groupName, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/aws/groups_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestNewGroup(t *testing.T) { 24 | g := NewGroup("test_group@example.com") 25 | 26 | assert.Len(t, g.Schemas, 1) 27 | assert.Equal(t, g.Schemas[0], "urn:ietf:params:scim:schemas:core:2.0:Group") 28 | assert.Equal(t, g.DisplayName, "test_group@example.com") 29 | } 30 | -------------------------------------------------------------------------------- /internal/aws/http.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | import "net/http" 18 | 19 | // HTTPClient is a generic HTTP Do interface 20 | type HTTPClient interface { 21 | Do(req *http.Request) (*http.Response, error) 22 | } 23 | -------------------------------------------------------------------------------- /internal/aws/mock/mock_http.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mock 16 | 17 | import ( 18 | "net/http" 19 | "reflect" 20 | 21 | "github.com/golang/mock/gomock" 22 | ) 23 | 24 | // IHTTPClient is a mock of IHTTPClient interface 25 | type IHTTPClient struct { 26 | ctrl *gomock.Controller 27 | recorder *IHTTPClientMockRecorder 28 | } 29 | 30 | // IHTTPClientMockRecorder is the mock recorder for IHTTPClient 31 | type IHTTPClientMockRecorder struct { 32 | mock *IHTTPClient 33 | } 34 | 35 | // NewIHTTPClient creates a new mock instance 36 | func NewIHTTPClient(ctrl *gomock.Controller) *IHTTPClient { 37 | mock := &IHTTPClient{ctrl: ctrl} 38 | mock.recorder = &IHTTPClientMockRecorder{mock} 39 | return mock 40 | } 41 | 42 | // EXPECT returns an object that allows the caller to indicate expected use 43 | func (m *IHTTPClient) EXPECT() *IHTTPClientMockRecorder { 44 | return m.recorder 45 | } 46 | 47 | // Do mocks base method 48 | func (m *IHTTPClient) Do(req *http.Request) (*http.Response, error) { 49 | m.ctrl.T.Helper() 50 | ret := m.ctrl.Call(m, "Do", req) 51 | ret0, _ := ret[0].(*http.Response) 52 | ret1, _ := ret[1].(error) 53 | return ret0, ret1 54 | } 55 | 56 | // Do indicates an expected call of Do 57 | func (mr *IHTTPClientMockRecorder) Do(req interface{}) *gomock.Call { 58 | mr.mock.ctrl.T.Helper() 59 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*IHTTPClient)(nil).Do), req) 60 | } 61 | -------------------------------------------------------------------------------- /internal/aws/schema.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | // Group represents a Group in AWS SSO 18 | type Group struct { 19 | ID string `json:"id,omitempty"` 20 | Schemas []string `json:"schemas"` 21 | DisplayName string `json:"displayName"` 22 | Members []string `json:"members"` 23 | } 24 | 25 | // GroupFilterResults represents filtered results when we search for 26 | // groups or List all groups 27 | type GroupFilterResults struct { 28 | Schemas []string `json:"schemas"` 29 | TotalResults int `json:"totalResults"` 30 | ItemsPerPage int `json:"itemsPerPage"` 31 | StartIndex int `json:"startIndex"` 32 | Resources []Group `json:"Resources"` 33 | } 34 | 35 | // GroupMemberChangeMember is a value needed for the ID of the member 36 | // to add/remove 37 | type GroupMemberChangeMember struct { 38 | Value string `json:"value"` 39 | } 40 | 41 | // GroupMemberChangeOperation details the operation to take place on a group 42 | type GroupMemberChangeOperation struct { 43 | Operation string `json:"op"` 44 | Path string `json:"path"` 45 | Members []GroupMemberChangeMember `json:"value"` 46 | } 47 | 48 | // GroupMemberChange represents a change operation 49 | // for a group 50 | type GroupMemberChange struct { 51 | Schemas []string `json:"schemas"` 52 | Operations []GroupMemberChangeOperation `json:"Operations"` 53 | } 54 | 55 | // UserEmail represents a user email address 56 | type UserEmail struct { 57 | Value string `json:"value"` 58 | Type string `json:"type"` 59 | Primary bool `json:"primary"` 60 | } 61 | 62 | // UserAddress represents address values of users 63 | type UserAddress struct { 64 | Type string `json:"type"` 65 | } 66 | 67 | // User represents a User in AWS SSO 68 | type User struct { 69 | ID string `json:"id,omitempty"` 70 | Schemas []string `json:"schemas"` 71 | Username string `json:"userName"` 72 | Name struct { 73 | FamilyName string `json:"familyName"` 74 | GivenName string `json:"givenName"` 75 | } `json:"name"` 76 | DisplayName string `json:"displayName"` 77 | Active bool `json:"active"` 78 | Emails []UserEmail `json:"emails"` 79 | Addresses []UserAddress `json:"addresses"` 80 | } 81 | 82 | // UserFilterResults represents filtered results when we search for 83 | // users or List all users 84 | type UserFilterResults struct { 85 | Schemas []string `json:"schemas"` 86 | TotalResults int `json:"totalResults"` 87 | ItemsPerPage int `json:"itemsPerPage"` 88 | StartIndex int `json:"startIndex"` 89 | Resources []User `json:"Resources"` 90 | } 91 | -------------------------------------------------------------------------------- /internal/aws/users.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | import ( 18 | "strings" 19 | ) 20 | 21 | // NewUser creates a user object representing a user with the given 22 | // details. 23 | func NewUser(firstName string, lastName string, email string, active bool) *User { 24 | e := make([]UserEmail, 0) 25 | e = append(e, UserEmail{ 26 | Value: email, 27 | Type: "work", 28 | Primary: true, 29 | }) 30 | 31 | a := make([]UserAddress, 0) 32 | a = append(a, UserAddress{ 33 | Type: "work", 34 | }) 35 | 36 | return &User{ 37 | Schemas: []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, 38 | Username: email, 39 | Name: struct { 40 | FamilyName string `json:"familyName"` 41 | GivenName string `json:"givenName"` 42 | }{ 43 | FamilyName: lastName, 44 | GivenName: firstName, 45 | }, 46 | DisplayName: strings.Join([]string{firstName, lastName}, " "), 47 | Active: active, 48 | Emails: e, 49 | Addresses: a, 50 | } 51 | } 52 | 53 | // UpdateUser updates a user object representing a user with the given 54 | // details. 55 | func UpdateUser(id string, firstName string, lastName string, email string, active bool) *User { 56 | e := make([]UserEmail, 0) 57 | e = append(e, UserEmail{ 58 | Value: email, 59 | Type: "work", 60 | Primary: true, 61 | }) 62 | 63 | a := make([]UserAddress, 0) 64 | a = append(a, UserAddress{ 65 | Type: "work", 66 | }) 67 | 68 | return &User{ 69 | Schemas: []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, 70 | ID: id, 71 | Username: email, 72 | Name: struct { 73 | FamilyName string `json:"familyName"` 74 | GivenName string `json:"givenName"` 75 | }{ 76 | FamilyName: lastName, 77 | GivenName: firstName, 78 | }, 79 | DisplayName: strings.Join([]string{firstName, lastName}, " "), 80 | Active: active, 81 | Emails: e, 82 | Addresses: a, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/aws/users_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aws 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestNewUser(t *testing.T) { 24 | u := NewUser("Lee", "Packham", "test@email.com", true) 25 | assert.Equal(t, u.Name.GivenName, "Lee") 26 | assert.Equal(t, u.Name.FamilyName, "Packham") 27 | assert.Equal(t, u.DisplayName, "Lee Packham") 28 | assert.Len(t, u.Emails, 1) 29 | 30 | assert.Equal(t, u.Emails[0].Value, "test@email.com") 31 | assert.Equal(t, u.Emails[0].Primary, true) 32 | 33 | assert.Equal(t, u.Active, true) 34 | 35 | assert.Len(t, u.Schemas, 1) 36 | assert.Equal(t, u.Schemas[0], "urn:ietf:params:scim:schemas:core:2.0:User") 37 | } 38 | 39 | func TestUpdateUser(t *testing.T) { 40 | u := UpdateUser("111", "Lee", "Packham", "test@email.com", false) 41 | assert.Equal(t, u.Name.GivenName, "Lee") 42 | assert.Equal(t, u.Name.FamilyName, "Packham") 43 | assert.Equal(t, u.DisplayName, "Lee Packham") 44 | assert.Len(t, u.Emails, 1) 45 | 46 | assert.Equal(t, u.Emails[0].Value, "test@email.com") 47 | assert.Equal(t, u.Emails[0].Primary, true) 48 | 49 | assert.Equal(t, u.Active, false) 50 | 51 | assert.Len(t, u.Schemas, 1) 52 | assert.Equal(t, u.Schemas[0], "urn:ietf:params:scim:schemas:core:2.0:User") 53 | } 54 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | // Package config ... 2 | package config 3 | 4 | // Config ... 5 | type Config struct { 6 | // Verbose toggles the verbosity 7 | Debug bool 8 | // LogLevel is the level with with to log for this config 9 | LogLevel string `mapstructure:"log_level"` 10 | // LogFormat is the format that is used for logging 11 | LogFormat string `mapstructure:"log_format"` 12 | // GoogleCredentials ... 13 | GoogleCredentials string `mapstructure:"google_credentials"` 14 | // GoogleAdmin ... 15 | GoogleAdmin string `mapstructure:"google_admin"` 16 | // UserMatch ... 17 | UserMatch string `mapstructure:"user_match"` 18 | // GroupFilter ... 19 | GroupMatch string `mapstructure:"group_match"` 20 | // SCIMEndpoint .... 21 | SCIMEndpoint string `mapstructure:"scim_endpoint"` 22 | // SCIMAccessToken ... 23 | SCIMAccessToken string `mapstructure:"scim_access_token"` 24 | // IsLambda ... 25 | IsLambda bool 26 | // IsLambdaRunningInCodePipeline ... 27 | IsLambdaRunningInCodePipeline bool 28 | // Ignore users ... 29 | IgnoreUsers []string `mapstructure:"ignore_users"` 30 | // Ignore groups ... 31 | IgnoreGroups []string `mapstructure:"ignore_groups"` 32 | // Include groups ... 33 | IncludeGroups []string `mapstructure:"include_groups"` 34 | // SyncMethod allow to defined the sync method used to get the user and groups from Google Workspace 35 | SyncMethod string `mapstructure:"sync_method"` 36 | // Region is the region that the identity store exists on 37 | Region string `mapstructure:"region"` 38 | // IdentityStoreID is the ID of the identity store 39 | IdentityStoreID string `mapstructure:"identity_store_id"` 40 | } 41 | 42 | const ( 43 | // DefaultLogLevel is the default logging level. 44 | DefaultLogLevel = "info" 45 | // DefaultLogFormat is the default format of the logger 46 | DefaultLogFormat = "text" 47 | // DefaultDebug is the default debug status. 48 | DefaultDebug = false 49 | // DefaultGoogleCredentials is the default credentials path 50 | DefaultGoogleCredentials = "credentials.json" 51 | // DefaultSyncMethod is the default sync method to use. 52 | DefaultSyncMethod = "groups" 53 | ) 54 | 55 | // New returns a new Config 56 | func New() *Config { 57 | return &Config{ 58 | Debug: DefaultDebug, 59 | LogLevel: DefaultLogLevel, 60 | LogFormat: DefaultLogFormat, 61 | SyncMethod: DefaultSyncMethod, 62 | GoogleCredentials: DefaultGoogleCredentials, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/awslabs/ssosync/internal/config" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestConfig(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | cfg := New() 15 | 16 | assert.NotNil(cfg) 17 | 18 | assert.Equal(cfg.LogLevel, DefaultLogLevel) 19 | assert.Equal(cfg.LogFormat, DefaultLogFormat) 20 | assert.Equal(cfg.Debug, DefaultDebug) 21 | assert.Equal(cfg.GoogleCredentials, DefaultGoogleCredentials) 22 | } 23 | -------------------------------------------------------------------------------- /internal/config/secrets.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/aws/aws-sdk-go/aws" 6 | "github.com/aws/aws-sdk-go/service/secretsmanager" 7 | ) 8 | 9 | // Secrets ... 10 | type Secrets struct { 11 | svc *secretsmanager.SecretsManager 12 | } 13 | 14 | // NewSecrets ... 15 | func NewSecrets(svc *secretsmanager.SecretsManager) *Secrets { 16 | return &Secrets{ 17 | svc: svc, 18 | } 19 | } 20 | 21 | // GoogleAdminEmail ... 22 | func (s *Secrets) GoogleAdminEmail(secretArn string) (string, error) { 23 | if len([]rune(secretArn)) == 0 { 24 | return s.getSecret("SSOSyncGoogleAdminEmail") 25 | } 26 | return s.getSecret(secretArn) 27 | } 28 | 29 | // SCIMAccessToken ... 30 | func (s *Secrets) SCIMAccessToken(secretArn string) (string, error) { 31 | if len([]rune(secretArn)) == 0 { 32 | return s.getSecret("SSOSyncSCIMAccessToken") 33 | } 34 | return s.getSecret(secretArn) 35 | } 36 | 37 | // SCIMEndpointURL ... 38 | func (s *Secrets) SCIMEndpointURL(secretArn string) (string, error) { 39 | if len([]rune(secretArn)) == 0 { 40 | return s.getSecret("SSOSyncSCIMEndpointURL") 41 | } 42 | return s.getSecret(secretArn) 43 | } 44 | 45 | // GoogleCredentials ... 46 | func (s *Secrets) GoogleCredentials(secretArn string) (string, error) { 47 | if len([]rune(secretArn)) == 0 { 48 | return s.getSecret("SSOSyncGoogleCredentials") 49 | } 50 | return s.getSecret(secretArn) 51 | } 52 | 53 | // Region ... 54 | func (s *Secrets) Region(secretArn string) (string, error) { 55 | if len([]rune(secretArn)) == 0 { 56 | return s.getSecret("SSOSyncRegion") 57 | } 58 | return s.getSecret(secretArn) 59 | } 60 | 61 | // IdentityStoreID ... 62 | func (s *Secrets) IdentityStoreID(secretArn string) (string, error) { 63 | if len([]rune(secretArn)) == 0 { 64 | return s.getSecret("IdentityStoreID") 65 | } 66 | return s.getSecret(secretArn) 67 | } 68 | 69 | func (s *Secrets) getSecret(secretKey string) (string, error) { 70 | r, err := s.svc.GetSecretValue(&secretsmanager.GetSecretValueInput{ 71 | SecretId: aws.String(secretKey), 72 | VersionStage: aws.String("AWSCURRENT"), 73 | }) 74 | 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | var secretString string 80 | 81 | if r.SecretString != nil { 82 | secretString = *r.SecretString 83 | } else { 84 | decodedBinarySecretBytes := make([]byte, base64.StdEncoding.DecodedLen(len(r.SecretBinary))) 85 | l, err := base64.StdEncoding.Decode(decodedBinarySecretBytes, r.SecretBinary) 86 | if err != nil { 87 | return "", err 88 | } 89 | secretString = string(decodedBinarySecretBytes[:l]) 90 | } 91 | 92 | return secretString, nil 93 | } 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /internal/google/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package google ... 16 | package google 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "strings" 22 | 23 | "golang.org/x/oauth2/google" 24 | admin "google.golang.org/api/admin/directory/v1" 25 | "google.golang.org/api/option" 26 | ) 27 | 28 | // Client is the Interface for the Client 29 | type Client interface { 30 | GetUsers(string) ([]*admin.User, error) 31 | GetDeletedUsers() ([]*admin.User, error) 32 | GetGroups(string) ([]*admin.Group, error) 33 | GetGroupMembers(*admin.Group) ([]*admin.Member, error) 34 | } 35 | 36 | type client struct { 37 | ctx context.Context 38 | service *admin.Service 39 | } 40 | 41 | // NewClient creates a new client for Google's Admin API 42 | func NewClient(ctx context.Context, adminEmail string, serviceAccountKey []byte) (Client, error) { 43 | config, err := google.JWTConfigFromJSON(serviceAccountKey, admin.AdminDirectoryGroupReadonlyScope, 44 | admin.AdminDirectoryGroupMemberReadonlyScope, 45 | admin.AdminDirectoryUserReadonlyScope) 46 | 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | config.Subject = adminEmail 52 | 53 | ts := config.TokenSource(ctx) 54 | 55 | srv, err := admin.NewService(ctx, option.WithTokenSource(ts)) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &client{ 61 | ctx: ctx, 62 | service: srv, 63 | }, nil 64 | } 65 | 66 | // GetDeletedUsers will get the deleted users from the Google's Admin API. 67 | func (c *client) GetDeletedUsers() ([]*admin.User, error) { 68 | u := make([]*admin.User, 0) 69 | var err error 70 | 71 | err = c.service.Users.List().Customer("my_customer").ShowDeleted("true").Pages(c.ctx, func(users *admin.Users) error { 72 | if err != nil { 73 | return err 74 | } 75 | u = append(u, users.Users...) 76 | return nil 77 | }) 78 | 79 | return u, err 80 | } 81 | 82 | // GetGroupMembers will get the members of the group specified 83 | func (c *client) GetGroupMembers(g *admin.Group) ([]*admin.Member, error) { 84 | m := make([]*admin.Member, 0) 85 | var err error 86 | 87 | err = c.service.Members.List(g.Id).Pages(context.TODO(), func(members *admin.Members) error { 88 | if err != nil { 89 | return err 90 | } 91 | m = append(m, members.Members...) 92 | return nil 93 | }) 94 | 95 | return m, err 96 | } 97 | 98 | // GetUsers will get the users from Google's Admin API 99 | // using the Method: users.list with parameter "query" 100 | // References: 101 | // * https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list 102 | // * https://developers.google.com/admin-sdk/directory/v1/guides/search-users 103 | // query possible values: 104 | // '' --> empty or not defined 105 | // name:'Jane' 106 | // email:admin* 107 | // isAdmin=true 108 | // manager='janesmith@example.com' 109 | // orgName=Engineering orgTitle:Manager 110 | // EmploymentData.projects:'GeneGnomes' 111 | func (c *client) GetUsers(query string) ([]*admin.User, error) { 112 | u := make([]*admin.User, 0) 113 | var err error 114 | 115 | // If we have an empty query, return nothing. 116 | if query == "" { 117 | return u, err 118 | } 119 | 120 | // If we have wildcard then fetch all users 121 | if query == "*" { 122 | err = c.service.Users.List().Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error { 123 | if err != nil { 124 | return err 125 | } 126 | u = append(u, users.Users...) 127 | return nil 128 | }) 129 | } else { 130 | 131 | // The Google api doesn't support multi-part queries, but we do so we need to split into an array of query strings 132 | queries := strings.Split(query, ",") 133 | 134 | // Then call the api one query at a time, appending to our list 135 | for _, subQuery := range queries { 136 | err = c.service.Users.List().Query(subQuery).Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error { 137 | if err != nil { 138 | return err 139 | } 140 | u = append(u, users.Users...) 141 | return nil 142 | }) 143 | } 144 | } 145 | 146 | // some people prefer to go by a mononym 147 | // Google directory will accept a 'zero width space' for an empty name but will not accept a 'space' 148 | // but 149 | // Identity Store will accept and a 'space' for an empty name but not a 'zero width space' 150 | // So we need to replace any 'zero width space' strings with a single 'space' to allow comparison and sync 151 | for _, user := range u { 152 | user.Name.GivenName = strings.Replace(user.Name.GivenName, string('\u200B'), " ", -1) 153 | user.Name.FamilyName = strings.Replace(user.Name.FamilyName, string('\u200B'), " ", -1) 154 | } 155 | 156 | // Check we've got some users otherwise something is wrong. 157 | if len(u) == 0 { 158 | return u, errors.New("google api returned 0 users?") 159 | } 160 | return u, err 161 | 162 | } 163 | 164 | // GetGroups will get the groups from Google's Admin API 165 | // using the Method: groups.list with parameter "query" 166 | // References: 167 | // * https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list 168 | // * https://developers.google.com/admin-sdk/directory/v1/guides/search-groups 169 | // query possible values: 170 | // '' --> empty or not defined 171 | // name='contact' 172 | // email:admin* 173 | // memberKey=user@company.com 174 | // name:contact* email:contact* 175 | // name:Admin* email:aws-* 176 | // email:aws-* 177 | func (c *client) GetGroups(query string) ([]*admin.Group, error) { 178 | g := make([]*admin.Group, 0) 179 | var err error 180 | 181 | // If we have an empty query, then we are not looking for groups 182 | if query == "" { 183 | return g, err 184 | } 185 | 186 | // If we have wildcard then fetch all groups 187 | if query == "*" { 188 | err = c.service.Groups.List().Customer("my_customer").Pages(context.TODO(), func(groups *admin.Groups) error { 189 | if err != nil { 190 | return err 191 | } 192 | g = append(g, groups.Groups...) 193 | return nil 194 | }) 195 | return g, err 196 | } 197 | 198 | // The Google api doesn't support multi-part queries, but we do so we need to split into an array of query strings 199 | queries := strings.Split(query, ",") 200 | 201 | // Then call the api one query at a time, appending to our list 202 | for _, subQuery := range queries { 203 | err = c.service.Groups.List().Customer("my_customer").Query(subQuery).Pages(context.TODO(), func(groups *admin.Groups) error { 204 | if err != nil { 205 | return err 206 | } 207 | g = append(g, groups.Groups...) 208 | return nil 209 | }) 210 | } 211 | 212 | // Check we've got some users otherwise something is wrong. 213 | if len(g) == 0 { 214 | return g, errors.New("google api return 0 groups?") 215 | } 216 | return g, err 217 | } 218 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Amazon.com, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "math/rand" 19 | "time" 20 | 21 | "github.com/awslabs/ssosync/cmd" 22 | ) 23 | 24 | func init() { 25 | rand.Seed(time.Now().UnixNano()) 26 | } 27 | 28 | func main() { 29 | cmd.Execute() 30 | } 31 | -------------------------------------------------------------------------------- /master: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/ssosync/beff54f81747f1b1c31553f161d885b00683b3d5/master --------------------------------------------------------------------------------