├── .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 | 
4 | 
5 | [](https://goreportcard.com/report/github.com/awslabs/ssosync)
6 | [](https://www.apache.org/licenses/LICENSE-2.0)
7 | [](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
--------------------------------------------------------------------------------