├── .buildspec.yaml ├── .dockerignore ├── .github └── workflows │ └── dependabot.yaml ├── .gitignore ├── .gitlab-ci.yml ├── .idea ├── cfn-saml-provider.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── .make-release-support ├── .pep8.rc ├── .release ├── Dockerfile ├── LICENSE ├── Makefile ├── Makefile.mk ├── Pipfile ├── Pipfile.lock ├── README.md ├── cloudformation ├── cfn-resource-provider.yaml ├── cicd-pipeline.yaml └── demo-stack.yaml ├── docs └── saml-provider.md ├── requirements.txt ├── src ├── __init__.py └── saml_provider.py ├── test-requirements.txt └── tests └── test_saml_provider.py /.buildspec.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 0.2 3 | phases: 4 | install: 5 | runtime-versions: 6 | python: 3.9 7 | build: 8 | commands: 9 | - make ecr-login 10 | - make snapshot 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | db.sqllite3 3 | mysql-data/ 4 | .git/ 5 | staticfiles/ 6 | venv/ 7 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Merge dependabot PRs 3 | 4 | on: pull_request_target 5 | 6 | permissions: 7 | pull-requests: write 8 | contents: write 9 | 10 | jobs: 11 | dependabot: 12 | runs-on: ubuntu-latest 13 | if: ${{ github.actor == 'dependabot[bot]' }} 14 | steps: 15 | - name: Dependabot metadata 16 | id: dependabot-metadata 17 | uses: dependabot/fetch-metadata@v1 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | - name: Approve a PR 21 | run: gh pr review --approve "$PR_URL" 22 | env: 23 | PR_URL: ${{ github.event.pull_request.html_url }} 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | - name: Enable auto-merge for Dependabot PRs 26 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 27 | run: gh pr merge --auto --squash "$PR_URL" 28 | env: 29 | PR_URL: ${{ github.event.pull_request.html_url }} 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache/ 2 | tests/.pytest_cache/ 3 | .idea/workspace.xml 4 | .idea/tasks.xml 5 | .idea/dbnavigator.xml 6 | .noseids 7 | *.zip 8 | deps/ 9 | .vscode/ 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | env/ 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # dotenv 92 | .env 93 | 94 | # virtualenv 95 | .venv 96 | venv/ 97 | ENV/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | build: 3 | stage: build 4 | script: 5 | - make S3_BUCKET_PREFIX=${LAMBDA_BUCKET_PREFIX:-binxio-public} AWS_REGION=${LAMBDA_BUCKET_REGION:-eu-central-1} deploy 6 | -------------------------------------------------------------------------------- /.idea/cfn-saml-provider.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.make-release-support: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Xebia Nederland B.V. 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 | function hasChanges() { 16 | test -n "$(showChanges)" 17 | } 18 | 19 | function showChanges() { 20 | git status -s -- $(getTagOnChangesIn) 21 | } 22 | 23 | function getTagOnChangesIn() { 24 | result=$(awk -F= '/^tag_on_changes_in=/{print $2}' .release) 25 | if egrep -q -e '^\.$' -e '^\./?[ ]' -e '[ ]\./?[ ]' <<< "$result" ; then 26 | echo "$result" 27 | else 28 | echo ". $result" 29 | fi 30 | return 0 31 | } 32 | 33 | function getRelease() { 34 | awk -F= '/^release=/{print $2}' .release 35 | } 36 | 37 | function getBaseTag() { 38 | sed -n -e "s/^tag=\(.*\)$(getRelease)\$/\1/p" .release 39 | } 40 | 41 | function getTag() { 42 | if [ -z "$1" ] ; then 43 | awk -F= '/^tag/{print $2}' .release 44 | else 45 | echo "$(getBaseTag)$1" 46 | fi 47 | } 48 | 49 | function setRelease() { 50 | if [ -n "$1" ] ; then 51 | sed -i.x -e "s~^tag=.*~tag=$(getTag $1)~" .release 52 | sed -i.x -e "s~^release=.*~release=$1~g" .release 53 | rm -f .release.x 54 | runPreTagCommand "$1" 55 | else 56 | echo "ERROR: missing release version parameter " >&2 57 | return 1 58 | fi 59 | } 60 | 61 | function runPreTagCommand() { 62 | if [ -n "$1" ] ; then 63 | COMMAND=$(sed -n -e "s/@@RELEASE@@/$1/g" -e 's/^pre_tag_command=\(.*\)/\1/p' .release) 64 | if [ -n "$COMMAND" ] ; then 65 | if ! OUTPUT=$(bash -c "$COMMAND" 2>&1) ; then echo $OUTPUT >&2 && exit 1 ; fi 66 | fi 67 | else 68 | echo "ERROR: missing release version parameter " >&2 69 | return 1 70 | fi 71 | } 72 | 73 | function tagExists() { 74 | tag=${1:-$(getTag)} 75 | test -n "$tag" && test -n "$(git tag | grep "^$tag\$")" 76 | } 77 | 78 | function showDiffFromRelease() { 79 | git diff --compact-summary -r $tag -- $(getTagOnChangesIn) 80 | } 81 | 82 | function differsFromRelease() { 83 | tag=$(getTag) 84 | ! tagExists $tag || test -n "$(showDiffFromRelease)" 85 | } 86 | 87 | function getVersion() { 88 | result=$(getRelease) 89 | 90 | if differsFromRelease; then 91 | result="$result-$(git log -n 1 --format=%h $(getTagOnChangesIn))" 92 | fi 93 | 94 | if hasChanges ; then 95 | result="$result-dirty" 96 | fi 97 | echo $result 98 | } 99 | 100 | function nextPatchLevel() { 101 | version=${1:-$(getRelease)} 102 | major_and_minor=$(echo $version | cut -d. -f1,2) 103 | patch=$(echo $version | cut -d. -f3) 104 | version=$(printf "%s.%d" $major_and_minor $(($patch + 1))) 105 | echo $version 106 | } 107 | 108 | function nextMinorLevel() { 109 | version=${1:-$(getRelease)} 110 | major=$(echo $version | cut -d. -f1); 111 | minor=$(echo $version | cut -d. -f2); 112 | version=$(printf "%d.%d.0" $major $(($minor + 1))) ; 113 | echo $version 114 | } 115 | 116 | function nextMajorLevel() { 117 | version=${1:-$(getRelease)} 118 | major=$(echo $version | cut -d. -f1); 119 | version=$(printf "%d.0.0" $(($major + 1))) 120 | echo $version 121 | } 122 | -------------------------------------------------------------------------------- /.pep8.rc: -------------------------------------------------------------------------------- 1 | [pep8] 2 | #ignore = E226,E302,E41 3 | max-line-length = 132 4 | -------------------------------------------------------------------------------- /.release: -------------------------------------------------------------------------------- 1 | release=2.0.0 2 | tag=v2.0.0 3 | pre_tag_command=sed -i '' -e 's^cfn-saml-provider:[0-9a-z][0-9a-z._-]*^cfn-saml-provider:@@RELEASE@@^' README.md cloudformation/cfn-resource-provider.yaml 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.9 2 | 3 | WORKDIR ${LAMBDA_TASK_ROOT} 4 | 5 | COPY requirements.txt . 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | COPY src/ ./ 10 | 11 | RUN find . -type d -print0 | xargs -0 chmod ugo+rx && \ 12 | find . -type f -print0 | xargs -0 chmod ugo+r 13 | 14 | CMD ["saml_provider.handler"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include Makefile.mk 2 | 3 | USERNAME=xebia 4 | NAME=cfn-saml-provider 5 | 6 | AWS_REGION=eu-central-1 7 | AWS_ACCOUNT=$(shell aws sts get-caller-identity --query Account --output text) 8 | REGISTRY_HOST=$(AWS_ACCOUNT).dkr.ecr.$(AWS_REGION).amazonaws.com 9 | IMAGE=$(REGISTRY_HOST)/$(USERNAME)/$(NAME) 10 | TAG_WITH_LATEST=never 11 | 12 | 13 | requirements.txt test-requirements.txt: Pipfile.lock 14 | pipenv requirements > requirements.txt 15 | pipenv requirements --dev-only > test-requirements.txt 16 | 17 | Pipfile.lock: Pipfile 18 | pipenv update 19 | 20 | test: Pipfile.lock 21 | for n in ./cloudformation/*.yaml ; do aws cloudformation validate-template --template-body file://$$n ; done 22 | PYTHONPATH=$(PWD)/src pipenv run pytest ./tests/test*.py 23 | 24 | pre-build: requirements.txt 25 | 26 | 27 | fmt: 28 | black src/*.py tests/*.py 29 | 30 | deploy-provider: ## deploy the provider to the current account 31 | sed -i '' -e 's^$(NAME):[0-9]*\.[0-9]*\.[0-9]*[^\.]*^$(NAME):$(VERSION)^' cloudformation/cfn-resource-provider.yaml 32 | aws cloudformation deploy \ 33 | --stack-name $(NAME) \ 34 | --capabilities CAPABILITY_IAM \ 35 | --template-file ./cloudformation/cfn-resource-provider.yaml 36 | 37 | delete-provider: ## delete provider from the current account 38 | aws cloudformation delete-stack --stack-name $(NAME) 39 | aws cloudformation wait stack-delete-complete --stack-name $(NAME) 40 | 41 | 42 | 43 | deploy-pipeline: ## deploy the CI/CD pipeline 44 | aws cloudformation deploy \ 45 | --stack-name $(NAME)-pipeline \ 46 | --capabilities CAPABILITY_IAM \ 47 | --template-file ./cloudformation/cicd-pipeline.yaml \ 48 | --parameter-overrides Name=$(NAME) 49 | 50 | delete-pipeline: ## delete the CI/CD pipeline 51 | aws cloudformation delete-stack --stack-name $(NAME)-pipeline 52 | aws cloudformation wait stack-delete-complete --stack-name $(NAME)-pipeline 53 | 54 | demo: ## deploy the demo 55 | aws cloudformation deploy \ 56 | --stack-name $(NAME)-demo \ 57 | --capabilities CAPABILITY_NAMED_IAM \ 58 | --template-file ./cloudformation/demo-stack.yaml 59 | 60 | delete-demo: ## delete the demo 61 | aws cloudformation delete-stack --stack-name $(NAME)-demo 62 | aws cloudformation wait stack-delete-complete --stack-name $(NAME)-demo 63 | 64 | ecr-login: ## login to the ECR repository 65 | aws ecr get-login-password --region $(AWS_REGION) | \ 66 | docker login --username AWS --password-stdin $(REGISTRY_HOST) 67 | -------------------------------------------------------------------------------- /Makefile.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2024 Xebia Nederland B.V. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | REGISTRY_HOST=docker.io 17 | USERNAME=$(USER) 18 | NAME=$(shell basename $(CURDIR)) 19 | 20 | RELEASE_SUPPORT := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))/.make-release-support 21 | IMAGE=$(REGISTRY_HOST)/$(USERNAME)/$(NAME) 22 | 23 | 24 | VERSION=$(shell . $(RELEASE_SUPPORT) ; getVersion) 25 | BASE_RELEASE=$(shell . $(RELEASE_SUPPORT) ; getRelease) 26 | 27 | TAG=$(shell . $(RELEASE_SUPPORT); getTag) 28 | TAG_WITH_LATEST=always 29 | 30 | SHELL=/bin/bash 31 | 32 | DOCKER_BUILD_CONTEXT=. 33 | DOCKER_FILE_PATH=Dockerfile 34 | 35 | .PHONY: pre-build docker-build post-build build release patch-release minor-release major-release tag check-status check-release showver \ 36 | push pre-push do-push post-push showimage 37 | 38 | build: pre-build docker-build post-build ## builds a new version of your container image 39 | 40 | pre-build: 41 | 42 | 43 | post-build: 44 | 45 | 46 | pre-push: 47 | 48 | 49 | post-push: 50 | 51 | 52 | docker-build: .release 53 | docker build $(DOCKER_BUILD_ARGS) -t $(IMAGE):$(VERSION) $(DOCKER_BUILD_CONTEXT) -f $(DOCKER_FILE_PATH) 54 | @if [[ $(TAG_WITH_LATEST) != never ]] && ([[ $(TAG_WITH_LATEST) == always ]] || [[ $(BASE_RELEASE) == $(VERSION) ]]); then \ 55 | echo docker tag $(IMAGE):$(VERSION) $(IMAGE):latest >&2; \ 56 | docker tag $(IMAGE):$(VERSION) $(IMAGE):latest; \ 57 | else \ 58 | echo docker rmi --force --no-prune $(IMAGE):latest >&2; \ 59 | docker rmi --force --no-prune $(IMAGE):latest 2>/dev/null; \ 60 | fi 61 | 62 | .release: 63 | @echo "release=0.0.0" > .release 64 | @echo "tag=$(NAME)-0.0.0" >> .release 65 | @echo "tag_on_changes_in=." >> .release 66 | @echo INFO: .release created 67 | @cat .release 68 | 69 | 70 | release: check-status check-release build push 71 | 72 | 73 | push: pre-push do-push post-push 74 | 75 | do-push: IMAGE_EXISTS=$(shell docker manifest inspect $(IMAGE):$(VERSION) 2>/dev/null) 76 | do-push: 77 | docker push $(IMAGE):$(VERSION) 78 | @if [[ $(TAG_WITH_LATEST) != never ]] && ([[ $(TAG_WITH_LATEST) == always ]] || [[ $(BASE_RELEASE) == $(VERSION) ]]); then \ 79 | echo docker push $(IMAGE):latest >&2; \ 80 | docker push $(IMAGE):latest; \ 81 | fi 82 | 83 | snapshot: build push ## builds a new version of your container image, and pushes it to the registry 84 | 85 | showver: .release ## shows the current release tag based on the workspace 86 | @. $(RELEASE_SUPPORT); getVersion 87 | 88 | showimage: .release ## shows the container image name based on the workspace 89 | @echo $(IMAGE):$(VERSION) 90 | 91 | tag-patch-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextPatchLevel) 92 | tag-patch-release: .release tag ## increments the patch release level and create the tag without build 93 | 94 | tag-minor-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextMinorLevel) 95 | tag-minor-release: .release tag ## increments the minor release level and create the tag without build 96 | 97 | tag-major-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextMajorLevel) 98 | tag-major-release: .release tag ## increments the major release level and create the tag without build 99 | 100 | patch-release: tag-patch-release release ## increments the patch release level, build and push to registry 101 | @echo $(VERSION) 102 | 103 | minor-release: tag-minor-release release ## increments the minor release level, build and push to registry 104 | @echo $(VERSION) 105 | 106 | major-release: tag-major-release release ## increments the major release level, build and push to registry 107 | @echo $(VERSION) 108 | 109 | 110 | tag: TAG=$(shell . $(RELEASE_SUPPORT); getTag $(VERSION)) 111 | tag: check-status 112 | @. $(RELEASE_SUPPORT) ; ! tagExists $(TAG) || (echo "ERROR: tag $(TAG) for version $(VERSION) already tagged in git" >&2 && exit 1) ; 113 | @. $(RELEASE_SUPPORT) ; setRelease $(VERSION) 114 | git add . 115 | git commit -m "bumped to version $(VERSION)" ; 116 | git tag $(TAG) ; 117 | @ if [ -n "$(shell git remote -v)" ] ; then git push --tags ; else echo 'no remote to push tags to' ; fi 118 | 119 | check-status: ## checks whether there are outstanding changes 120 | @. $(RELEASE_SUPPORT) ; ! hasChanges || (echo "ERROR: there are still outstanding changes" >&2 && showChanges >&2 && exit 1) ; 121 | 122 | check-release: .release ## checks whether the workspace matches the tagged release in git 123 | @. $(RELEASE_SUPPORT) ; tagExists $(TAG) || (echo "ERROR: version not yet tagged in git. make [minor,major,patch]-release." >&2 && exit 1) ; 124 | @. $(RELEASE_SUPPORT) ; ! differsFromRelease $(TAG) || (echo "ERROR: current directory differs from tagged $(TAG). make [minor,major,patch]-release." && showDiffFromRelease >&2 ; exit 1) 125 | 126 | 127 | help: ## show this help. 128 | @fgrep -h "##" $(MAKEFILE_LIST) | grep -v fgrep | sed -e 's/\([^:]*\):[^#]*##\(.*\)/printf '"'%-20s - %s\\\\n' '\1' '\2'"'/' |bash 129 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [packages] 7 | cfn_resource_provider = ">=0.9.0" 8 | requests = "*" 9 | boto3 = "*" 10 | 11 | [dev-packages] 12 | pytest = "*" 13 | 14 | [requires] 15 | python_version = "3.9" 16 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "abd5222a379d4b3a7c15172fe0a3ac0b2a286a40bf6bc8124642d57eabea0284" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "attrs": { 20 | "hashes": [ 21 | "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", 22 | "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" 23 | ], 24 | "markers": "python_version >= '3.7'", 25 | "version": "==24.2.0" 26 | }, 27 | "boto3": { 28 | "hashes": [ 29 | "sha256:9edf49640c79a05b0a72f4c2d1e24dfc164344b680535a645f455ac624dc3680", 30 | "sha256:db58348849a5af061f0f5ec9c3b699da5221ca83354059fdccb798e3ddb6b62a" 31 | ], 32 | "index": "pypi", 33 | "markers": "python_version >= '3.8'", 34 | "version": "==1.35.57" 35 | }, 36 | "botocore": { 37 | "hashes": [ 38 | "sha256:92ddd02469213766872cb2399269dd20948f90348b42bf08379881d5e946cc34", 39 | "sha256:d96306558085baf0bcb3b022d7a8c39c93494f031edb376694d2b2dcd0e81327" 40 | ], 41 | "markers": "python_version >= '3.8'", 42 | "version": "==1.35.57" 43 | }, 44 | "certifi": { 45 | "hashes": [ 46 | "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", 47 | "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" 48 | ], 49 | "markers": "python_version >= '3.6'", 50 | "version": "==2024.8.30" 51 | }, 52 | "cfn-resource-provider": { 53 | "hashes": [ 54 | "sha256:41f5b3f41ccd208cbb8930118860af6afc21a7851f51fab251955c8f930e1ad2" 55 | ], 56 | "index": "pypi", 57 | "version": "==1.2.1" 58 | }, 59 | "charset-normalizer": { 60 | "hashes": [ 61 | "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", 62 | "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", 63 | "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", 64 | "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", 65 | "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", 66 | "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", 67 | "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", 68 | "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", 69 | "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", 70 | "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", 71 | "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", 72 | "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", 73 | "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", 74 | "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", 75 | "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", 76 | "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", 77 | "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", 78 | "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", 79 | "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", 80 | "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", 81 | "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", 82 | "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", 83 | "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", 84 | "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", 85 | "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", 86 | "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", 87 | "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", 88 | "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", 89 | "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", 90 | "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", 91 | "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", 92 | "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", 93 | "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", 94 | "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", 95 | "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", 96 | "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", 97 | "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", 98 | "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", 99 | "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", 100 | "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", 101 | "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", 102 | "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", 103 | "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", 104 | "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", 105 | "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", 106 | "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", 107 | "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", 108 | "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", 109 | "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", 110 | "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", 111 | "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", 112 | "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", 113 | "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", 114 | "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", 115 | "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", 116 | "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", 117 | "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", 118 | "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", 119 | "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", 120 | "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", 121 | "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", 122 | "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", 123 | "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", 124 | "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", 125 | "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", 126 | "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", 127 | "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", 128 | "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", 129 | "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", 130 | "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", 131 | "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", 132 | "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", 133 | "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", 134 | "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", 135 | "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", 136 | "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", 137 | "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", 138 | "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", 139 | "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", 140 | "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", 141 | "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", 142 | "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", 143 | "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", 144 | "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", 145 | "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", 146 | "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", 147 | "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", 148 | "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", 149 | "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", 150 | "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", 151 | "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", 152 | "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", 153 | "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", 154 | "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", 155 | "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", 156 | "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", 157 | "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", 158 | "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", 159 | "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", 160 | "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", 161 | "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", 162 | "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", 163 | "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", 164 | "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", 165 | "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" 166 | ], 167 | "markers": "python_full_version >= '3.7.0'", 168 | "version": "==3.4.0" 169 | }, 170 | "idna": { 171 | "hashes": [ 172 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 173 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 174 | ], 175 | "markers": "python_version >= '3.6'", 176 | "version": "==3.10" 177 | }, 178 | "jmespath": { 179 | "hashes": [ 180 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 181 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 182 | ], 183 | "markers": "python_version >= '3.7'", 184 | "version": "==1.0.1" 185 | }, 186 | "jsonschema": { 187 | "hashes": [ 188 | "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", 189 | "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" 190 | ], 191 | "markers": "python_version >= '3.8'", 192 | "version": "==4.23.0" 193 | }, 194 | "jsonschema-specifications": { 195 | "hashes": [ 196 | "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", 197 | "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" 198 | ], 199 | "markers": "python_version >= '3.9'", 200 | "version": "==2024.10.1" 201 | }, 202 | "python-dateutil": { 203 | "hashes": [ 204 | "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", 205 | "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" 206 | ], 207 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 208 | "version": "==2.9.0.post0" 209 | }, 210 | "referencing": { 211 | "hashes": [ 212 | "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", 213 | "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" 214 | ], 215 | "markers": "python_version >= '3.8'", 216 | "version": "==0.35.1" 217 | }, 218 | "requests": { 219 | "extras": [ 220 | "security" 221 | ], 222 | "hashes": [ 223 | "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", 224 | "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" 225 | ], 226 | "index": "pypi", 227 | "markers": "python_version >= '3.8'", 228 | "version": "==2.32.3" 229 | }, 230 | "rpds-py": { 231 | "hashes": [ 232 | "sha256:031819f906bb146561af051c7cef4ba2003d28cff07efacef59da973ff7969ba", 233 | "sha256:0626238a43152918f9e72ede9a3b6ccc9e299adc8ade0d67c5e142d564c9a83d", 234 | "sha256:085ed25baac88953d4283e5b5bd094b155075bb40d07c29c4f073e10623f9f2e", 235 | "sha256:0a9e0759e7be10109645a9fddaaad0619d58c9bf30a3f248a2ea57a7c417173a", 236 | "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202", 237 | "sha256:1ff2eba7f6c0cb523d7e9cff0903f2fe1feff8f0b2ceb6bd71c0e20a4dcee271", 238 | "sha256:20cc1ed0bcc86d8e1a7e968cce15be45178fd16e2ff656a243145e0b439bd250", 239 | "sha256:241e6c125568493f553c3d0fdbb38c74babf54b45cef86439d4cd97ff8feb34d", 240 | "sha256:2c51d99c30091f72a3c5d126fad26236c3f75716b8b5e5cf8effb18889ced928", 241 | "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0", 242 | "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d", 243 | "sha256:30bdc973f10d28e0337f71d202ff29345320f8bc49a31c90e6c257e1ccef4333", 244 | "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e", 245 | "sha256:32eb88c30b6a4f0605508023b7141d043a79b14acb3b969aa0b4f99b25bc7d4a", 246 | "sha256:3b766a9f57663396e4f34f5140b3595b233a7b146e94777b97a8413a1da1be18", 247 | "sha256:3b929c2bb6e29ab31f12a1117c39f7e6d6450419ab7464a4ea9b0b417174f044", 248 | "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677", 249 | "sha256:3e53861b29a13d5b70116ea4230b5f0f3547b2c222c5daa090eb7c9c82d7f664", 250 | "sha256:40c91c6e34cf016fa8e6b59d75e3dbe354830777fcfd74c58b279dceb7975b75", 251 | "sha256:4991ca61656e3160cdaca4851151fd3f4a92e9eba5c7a530ab030d6aee96ec89", 252 | "sha256:4ab2c2a26d2f69cdf833174f4d9d86118edc781ad9a8fa13970b527bf8236027", 253 | "sha256:4e8921a259f54bfbc755c5bbd60c82bb2339ae0324163f32868f63f0ebb873d9", 254 | "sha256:4eb2de8a147ffe0626bfdc275fc6563aa7bf4b6db59cf0d44f0ccd6ca625a24e", 255 | "sha256:5145282a7cd2ac16ea0dc46b82167754d5e103a05614b724457cffe614f25bd8", 256 | "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44", 257 | "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3", 258 | "sha256:5555db3e618a77034954b9dc547eae94166391a98eb867905ec8fcbce1308d95", 259 | "sha256:58a0e345be4b18e6b8501d3b0aa540dad90caeed814c515e5206bb2ec26736fd", 260 | "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab", 261 | "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a", 262 | "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560", 263 | "sha256:6b4ef7725386dc0762857097f6b7266a6cdd62bfd209664da6712cb26acef035", 264 | "sha256:6bc0e697d4d79ab1aacbf20ee5f0df80359ecf55db33ff41481cf3e24f206919", 265 | "sha256:6dcc4949be728ede49e6244eabd04064336012b37f5c2200e8ec8eb2988b209c", 266 | "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266", 267 | "sha256:808f1ac7cf3b44f81c9475475ceb221f982ef548e44e024ad5f9e7060649540e", 268 | "sha256:8404b3717da03cbf773a1d275d01fec84ea007754ed380f63dfc24fb76ce4592", 269 | "sha256:878f6fea96621fda5303a2867887686d7a198d9e0f8a40be100a63f5d60c88c9", 270 | "sha256:8a7ff941004d74d55a47f916afc38494bd1cfd4b53c482b77c03147c91ac0ac3", 271 | "sha256:95a5bad1ac8a5c77b4e658671642e4af3707f095d2b78a1fdd08af0dfb647624", 272 | "sha256:97ef67d9bbc3e15584c2f3c74bcf064af36336c10d2e21a2131e123ce0f924c9", 273 | "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b", 274 | "sha256:98e4fe5db40db87ce1c65031463a760ec7906ab230ad2249b4572c2fc3ef1f9f", 275 | "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca", 276 | "sha256:9afe42102b40007f588666bc7de82451e10c6788f6f70984629db193849dced1", 277 | "sha256:9e20da3957bdf7824afdd4b6eeb29510e83e026473e04952dca565170cd1ecc8", 278 | "sha256:a017f813f24b9df929674d0332a374d40d7f0162b326562daae8066b502d0590", 279 | "sha256:a429b99337062877d7875e4ff1a51fe788424d522bd64a8c0a20ef3021fdb6ed", 280 | "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952", 281 | "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11", 282 | "sha256:a89a8ce9e4e75aeb7fa5d8ad0f3fecdee813802592f4f46a15754dcb2fd6b061", 283 | "sha256:a8eeec67590e94189f434c6d11c426892e396ae59e4801d17a93ac96b8c02a6c", 284 | "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74", 285 | "sha256:ad116dda078d0bc4886cb7840e19811562acdc7a8e296ea6ec37e70326c1b41c", 286 | "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94", 287 | "sha256:af4a644bf890f56e41e74be7d34e9511e4954894d544ec6b8efe1e21a1a8da6c", 288 | "sha256:b21747f79f360e790525e6f6438c7569ddbfb1b3197b9e65043f25c3c9b489d8", 289 | "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf", 290 | "sha256:b4de1da871b5c0fd5537b26a6fc6814c3cc05cabe0c941db6e9044ffbb12f04a", 291 | "sha256:b80b4690bbff51a034bfde9c9f6bf9357f0a8c61f548942b80f7b66356508bf5", 292 | "sha256:b876f2bc27ab5954e2fd88890c071bd0ed18b9c50f6ec3de3c50a5ece612f7a6", 293 | "sha256:b8f107395f2f1d151181880b69a2869c69e87ec079c49c0016ab96860b6acbe5", 294 | "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3", 295 | "sha256:c2b2f71c6ad6c2e4fc9ed9401080badd1469fa9889657ec3abea42a3d6b2e1ed", 296 | "sha256:c3761f62fcfccf0864cc4665b6e7c3f0c626f0380b41b8bd1ce322103fa3ef87", 297 | "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b", 298 | "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72", 299 | "sha256:cbd7504a10b0955ea287114f003b7ad62330c9e65ba012c6223dba646f6ffd05", 300 | "sha256:d167e4dbbdac48bd58893c7e446684ad5d425b407f9336e04ab52e8b9194e2ed", 301 | "sha256:d2132377f9deef0c4db89e65e8bb28644ff75a18df5293e132a8d67748397b9f", 302 | "sha256:da52d62a96e61c1c444f3998c434e8b263c384f6d68aca8274d2e08d1906325c", 303 | "sha256:daa8efac2a1273eed2354397a51216ae1e198ecbce9036fba4e7610b308b6153", 304 | "sha256:dc5695c321e518d9f03b7ea6abb5ea3af4567766f9852ad1560f501b17588c7b", 305 | "sha256:de552f4a1916e520f2703ec474d2b4d3f86d41f353e7680b597512ffe7eac5d0", 306 | "sha256:de609a6f1b682f70bb7163da745ee815d8f230d97276db049ab447767466a09d", 307 | "sha256:e12bb09678f38b7597b8346983d2323a6482dcd59e423d9448108c1be37cac9d", 308 | "sha256:e168afe6bf6ab7ab46c8c375606298784ecbe3ba31c0980b7dcbb9631dcba97e", 309 | "sha256:e78868e98f34f34a88e23ee9ccaeeec460e4eaf6db16d51d7a9b883e5e785a5e", 310 | "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd", 311 | "sha256:ea3a6ac4d74820c98fcc9da4a57847ad2cc36475a8bd9683f32ab6d47a2bd682", 312 | "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4", 313 | "sha256:ed6378c9d66d0de903763e7706383d60c33829581f0adff47b6535f1802fa6db", 314 | "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976", 315 | "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937", 316 | "sha256:efec946f331349dfc4ae9d0e034c263ddde19414fe5128580f512619abed05f1", 317 | "sha256:f414da5c51bf350e4b7960644617c130140423882305f7574b6cf65a3081cecb", 318 | "sha256:f71009b0d5e94c0e86533c0b27ed7cacc1239cb51c178fd239c3cfefefb0400a", 319 | "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7", 320 | "sha256:faa5e8496c530f9c71f2b4e1c49758b06e5f4055e17144906245c99fa6d45356", 321 | "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be" 322 | ], 323 | "markers": "python_version >= '3.9'", 324 | "version": "==0.21.0" 325 | }, 326 | "s3transfer": { 327 | "hashes": [ 328 | "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", 329 | "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" 330 | ], 331 | "markers": "python_version >= '3.8'", 332 | "version": "==0.10.3" 333 | }, 334 | "six": { 335 | "hashes": [ 336 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 337 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 338 | ], 339 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 340 | "version": "==1.16.0" 341 | }, 342 | "urllib3": { 343 | "hashes": [ 344 | "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", 345 | "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" 346 | ], 347 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 348 | "version": "==1.26.20" 349 | } 350 | }, 351 | "develop": { 352 | "exceptiongroup": { 353 | "hashes": [ 354 | "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", 355 | "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" 356 | ], 357 | "markers": "python_version < '3.11'", 358 | "version": "==1.2.2" 359 | }, 360 | "iniconfig": { 361 | "hashes": [ 362 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 363 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 364 | ], 365 | "markers": "python_version >= '3.7'", 366 | "version": "==2.0.0" 367 | }, 368 | "packaging": { 369 | "hashes": [ 370 | "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", 371 | "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" 372 | ], 373 | "markers": "python_version >= '3.8'", 374 | "version": "==24.2" 375 | }, 376 | "pluggy": { 377 | "hashes": [ 378 | "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", 379 | "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" 380 | ], 381 | "markers": "python_version >= '3.8'", 382 | "version": "==1.5.0" 383 | }, 384 | "pytest": { 385 | "hashes": [ 386 | "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", 387 | "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" 388 | ], 389 | "index": "pypi", 390 | "markers": "python_version >= '3.8'", 391 | "version": "==8.3.3" 392 | }, 393 | "tomli": { 394 | "hashes": [ 395 | "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", 396 | "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" 397 | ], 398 | "markers": "python_version < '3.11'", 399 | "version": "==2.0.2" 400 | } 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cfn-saml-provider 2 | A CloudFormation custom resource provider for adding a SAML identity provider. 3 | 4 | ## Syntax 5 | To create a SAML provider using your AWS CloudFormation template, use a [Custom::SAMLProvider](docs/saml-provider.md) resource with reference 6 | to the metadata URL: 7 | 8 | ```yaml 9 | SAMLProvider: 10 | Type: Custom::SAMLProvider 11 | Properties: 12 | Name: auth0 13 | URL: https://auth0.com/mytenant/providerurl 14 | ServiceToken: !Sub 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:cfn-saml-provider' 15 | ``` 16 | or with the metadata itself: 17 | 18 | ```yaml 19 | SAMLProvider: 20 | Type: Custom::SAMLProvider 21 | Properties: 22 | Name: auth0 23 | Metadata: | 24 | 25 | .... 26 | 27 | ServiceToken: !Sub 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:cfn-saml-provider' 28 | ``` 29 | 30 | On completion, it will return the ARN of the SAML Provider. 31 | 32 | ### Deploy the provider 33 | To deploy the provider, type: 34 | 35 | ```sh 36 | aws cloudformation create-stack \ 37 | --capabilities CAPABILITY_IAM \ 38 | --stack-name cfn-saml-provider \ 39 | --template-body file://cloudformation/cfn-saml-provider.json 40 | 41 | aws cloudformation wait stack-create-complete --stack-name cfn-saml-provider 42 | ``` 43 | 44 | This CloudFormation template will use our pre-packaged provider from `463637877380.dkr.ecr.eu-central-1.amazonaws.com/xebia/cfn-saml-provider:2.0.0`. 45 | 46 | ## Demo 47 | To install the simple sample of the SAML provider, type: 48 | 49 | ```sh 50 | aws cloudformation create-stack --stack-name cfn-saml-provider-demo \ 51 | --template-body file://cloudformation/demo-stack.json 52 | aws cloudformation wait stack-create-complete --stack-name cfn-saml-provider-demo 53 | ``` 54 | 55 | to validate the result, type: 56 | 57 | ```sh 58 | aws iam list-saml-providers 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /cloudformation/cfn-resource-provider.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: Auth0 Custom CloudFormation Provider 3 | Parameters: 4 | DefaultSecurityGroup: 5 | Type: String 6 | Default: '' 7 | S3BucketPrefix: 8 | Type: String 9 | Default: '' 10 | CFNCustomProviderZipFileName: 11 | Type: String 12 | Default: lambdas/cfn-saml-provider-1.0.0.zip 13 | Resources: 14 | CFNCustomProvider: 15 | Type: AWS::Lambda::Function 16 | DependsOn: 17 | - LambdaRole 18 | Properties: 19 | Description: Custom SAML Provider CloudFormation Provider 20 | PackageType: Image 21 | Code: 22 | ImageUri: 463637877380.dkr.ecr.eu-central-1.amazonaws.com/xebia/cfn-saml-provider:2.0.0 23 | FunctionName: cfn-saml-provider 24 | MemorySize: 128 25 | Timeout: 10 26 | Role: !GetAtt 'LambdaRole.Arn' 27 | LambdaPolicy: 28 | Type: AWS::IAM::Policy 29 | DependsOn: 30 | - LambdaRole 31 | Properties: 32 | PolicyName: CFNCustomProviderPolicy 33 | PolicyDocument: 34 | Version: '2012-10-17' 35 | Statement: 36 | - Effect: Allow 37 | Action: 38 | - iam:CreateSAMLProvider 39 | - iam:UpdateSAMLProvider 40 | - iam:DeleteSAMLProvider 41 | Resource: '*' 42 | Roles: 43 | - !Ref 'LambdaRole' 44 | 45 | LambdaRole: 46 | Type: AWS::IAM::Role 47 | Properties: 48 | ManagedPolicyArns: 49 | - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 50 | AssumeRolePolicyDocument: 51 | Version: '2012-10-17' 52 | Statement: 53 | - Action: 54 | - sts:AssumeRole 55 | Effect: Allow 56 | Principal: 57 | Service: 58 | - lambda.amazonaws.com 59 | -------------------------------------------------------------------------------- /cloudformation/cicd-pipeline.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | Name: 4 | Type: String 5 | 6 | Resources: 7 | Project: 8 | Type: AWS::CodeBuild::Project 9 | DependsOn: 10 | - LogGroup 11 | Properties: 12 | Name: !Ref Name 13 | Description: !Sub '${Name} builder' 14 | ServiceRole: !Ref 'ProjectRole' 15 | Artifacts: 16 | Type: NO_ARTIFACTS 17 | Environment: 18 | Type: LINUX_CONTAINER 19 | ComputeType: BUILD_GENERAL1_SMALL 20 | Image: aws/codebuild/standard:7.0 21 | PrivilegedMode: true 22 | Source: 23 | Type: GITHUB 24 | Location: !Sub 'https://github.com/binxio/${Name}.git' 25 | BuildSpec: .buildspec.yaml 26 | GitCloneDepth: 0 27 | Auth: 28 | Type: OAUTH 29 | Triggers: 30 | FilterGroups: 31 | - - Type: EVENT 32 | Pattern: PUSH 33 | - Type: HEAD_REF 34 | Pattern: "refs/tags/.*" 35 | Webhook: true 36 | 37 | ProjectRole: 38 | Type: AWS::IAM::Role 39 | Properties: 40 | AssumeRolePolicyDocument: 41 | Statement: 42 | - Effect: Allow 43 | Action: sts:AssumeRole 44 | Principal: 45 | Service: codebuild.amazonaws.com 46 | Condition: {} 47 | Path: / 48 | Policies: 49 | - PolicyName: LambdaBuilder 50 | PolicyDocument: 51 | Statement: 52 | - Effect: Allow 53 | Action: 54 | - ec2:DescribeRegions 55 | - ecr:GetAuthorizationToken 56 | Resource: 57 | - '*' 58 | 59 | - Effect: Allow 60 | Action: 61 | - ecr:GetDownloadUrlForLayer 62 | - ecr:BatchGetImage 63 | - ecr:BatchCheckLayerAvailability 64 | - ecr:PutImage 65 | - ecr:InitiateLayerUpload 66 | - ecr:UploadLayerPart 67 | - ecr:CompleteLayerUpload 68 | Resource: 69 | - !Sub 'arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/xebia/${Name}' 70 | 71 | - Sid: CloudWatchLogsPolicy 72 | Effect: Allow 73 | Action: 74 | - logs:CreateLogGroup 75 | - logs:CreateLogStream 76 | - logs:PutLogEvents 77 | Resource: 78 | - '*' 79 | 80 | Repository: 81 | Type: AWS::ECR::Repository 82 | Properties: 83 | RepositoryName: !Sub xebia/${Name} 84 | ImageTagMutability: IMMUTABLE 85 | RepositoryPolicyText: 86 | Version: '2012-10-17' 87 | Statement: 88 | - Sid: read only access 89 | Effect: Allow 90 | Principal: 91 | AWS: 92 | - '*' 93 | Action: 94 | - ecr:GetDownloadUrlForLayer 95 | - ecr:BatchGetImage 96 | - ecr:BatchCheckLayerAvailability 97 | 98 | - Sid: write to owner account 99 | Effect: Allow 100 | Principal: 101 | AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root' 102 | Action: 103 | - ecr:GetDownloadUrlForLayer 104 | - ecr:BatchGetImage 105 | - ecr:BatchCheckLayerAvailability 106 | - ecr:PutImage 107 | - ecr:InitiateLayerUpload 108 | - ecr:UploadLayerPart 109 | - ecr:CompleteLayerUpload 110 | 111 | LogGroup: 112 | Type: AWS::Logs::LogGroup 113 | Properties: 114 | LogGroupName: !Sub '/aws/codebuild/${Name}' 115 | RetentionInDays: 7 116 | 117 | -------------------------------------------------------------------------------- /cloudformation/demo-stack.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: Demo SAML Custom Provider 4 | 5 | Resources: 6 | SAMLProvider: 7 | Type: Custom::SAMLProvider 8 | Properties: 9 | ServiceToken: !Sub 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:cfn-saml-provider' 10 | Name: !Sub 'saml-provider-${AWS::StackName}' 11 | URL: 'https://mvanholsteijn.eu.auth0.com/samlp/metadata/ZALyvKmxYYlTmfHW4m4lzkia60S1YsSf' 12 | 13 | 14 | Outputs: 15 | SAMLProvider: 16 | Description: Arn of the SAML provider 17 | Value: !Ref 'SAMLProvider' 18 | -------------------------------------------------------------------------------- /docs/saml-provider.md: -------------------------------------------------------------------------------- 1 | # Custom::SAMLProvider 2 | The `Custom::SAMLProvider` creates IAM SAM Provider. 3 | 4 | ## Syntax 5 | To declare this entity in your AWS CloudFormation template, use the following syntax: 6 | 7 | ```yaml 8 | Type : Custom::SAMLProvider 9 | Properties 10 | Name: String 11 | Metadata: String 12 | URL: url 13 | ServiceToken" : !Sub 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:cfn-saml-provider' 14 | ``` 15 | 16 | It will create a SAML provider named `Name` using the `Metadata` literal or the content 17 | of the metadata `URL`. 18 | 19 | ## Properties 20 | You can specify the following properties: 21 | 22 | "Name" - of the SAML provider (required) 23 | "Metadata" - for the SAML Provider (required if URL is missing) 24 | "URL" - serving the metadata of the SAML Provider (required if Metadaga is missing) 25 | "ServiceToken" - pointing to the function implementing this (required) 26 | 27 | ## Return values 28 | The physical resource id is the ARN of the provider. There are no additional return attributes. 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | attrs==24.2.0; python_version >= '3.7' 3 | boto3==1.35.57; python_version >= '3.8' 4 | botocore==1.35.57; python_version >= '3.8' 5 | certifi==2024.8.30; python_version >= '3.6' 6 | cfn-resource-provider==1.2.1 7 | charset-normalizer==3.4.0; python_full_version >= '3.7.0' 8 | idna==3.10; python_version >= '3.6' 9 | jmespath==1.0.1; python_version >= '3.7' 10 | jsonschema==4.23.0; python_version >= '3.8' 11 | jsonschema-specifications==2024.10.1; python_version >= '3.9' 12 | python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' 13 | referencing==0.35.1; python_version >= '3.8' 14 | requests[security]==2.32.3; python_version >= '3.8' 15 | rpds-py==0.21.0; python_version >= '3.9' 16 | s3transfer==0.10.3; python_version >= '3.8' 17 | six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' 18 | urllib3==1.26.20; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' 19 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binxio/cfn-saml-provider/eab10ca4afda6273d44fa309c391ba2cb78de1a5/src/__init__.py -------------------------------------------------------------------------------- /src/saml_provider.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import boto3 4 | import requests 5 | import logging 6 | from botocore.exceptions import ClientError 7 | from cfn_resource_provider import ResourceProvider 8 | 9 | logging.basicConfig(level=os.getenv('LOG_LEVEL', 'INFO')) 10 | 11 | # 12 | # The request schema defining the Resource Properties 13 | # 14 | request_schema = { 15 | "type": "object", 16 | "oneOf": [{ 17 | "properties": { 18 | "Name": { 19 | "type": "string", 20 | "description": "of the saml provider" 21 | }, 22 | "Metadata": { 23 | "type": "string", 24 | "description": "of the saml provider" 25 | } 26 | }, 27 | "required": ["Name", "Metadata"] 28 | }, { 29 | "properties": { 30 | "Name": { 31 | "type": "string", 32 | "description": "of the saml provider" 33 | }, 34 | "URL": { 35 | "type": "string", 36 | "description": "pointing to the SAML Metadata Document" 37 | } 38 | }, 39 | "required": ["Name", "URL"] 40 | } 41 | ] 42 | } 43 | 44 | log = logging.getLogger(name=__name__) 45 | 46 | 47 | class SAMLProvider(ResourceProvider, object): 48 | """ 49 | Generic Cloudformation custom resource provider for Auth0 resources. 50 | """ 51 | 52 | def __init__(self): 53 | super(SAMLProvider, self).__init__() 54 | self.request_schema = request_schema 55 | self.iam = boto3.client('iam') 56 | 57 | @property 58 | def custom_cfn_resource_name(self): 59 | return 'Custom::SAMLProvider' 60 | 61 | def get_metadata(self): 62 | metadata = self.get('Metadata', None) 63 | if metadata != None: 64 | return None, metadata 65 | 66 | try: 67 | response = requests.get(self.get('URL')) 68 | metadata = response.text 69 | if response.status_code == 200: 70 | return None, metadata 71 | except requests.exceptions.RequestException as e: 72 | return '{}'.format(e), None 73 | 74 | return 'url %s returned status code %d, %s'.format(self.get('URL'), response.status_code, response.text), None 75 | 76 | def create(self): 77 | err, metadata = self.get_metadata() 78 | if err: 79 | self.physical_resource_id = 'could-not-create' 80 | self.fail(err) 81 | return 82 | 83 | try: 84 | response = self.iam.create_saml_provider(Name=self.get('Name'), SAMLMetadataDocument=metadata) 85 | self.physical_resource_id = response['SAMLProviderArn'] 86 | except ClientError as e: 87 | self.physical_resource_id = 'could-not-create' 88 | self.fail('{}'.format(e)) 89 | 90 | def update(self): 91 | err, metadata = self.get_metadata() 92 | if err: 93 | self.fail(err) 94 | return 95 | 96 | try: 97 | response = self.iam.update_saml_provider(SAMLProviderArn=self.physical_resource_id, SAMLMetadataDocument=metadata) 98 | self.physical_resource_id = response['SAMLProviderArn'] 99 | except ClientError as e: 100 | self.fail('{}'.format(e)) 101 | 102 | def delete(self): 103 | if re.match(r'arn:aws[-a-z]*:iam::[0-9]*:saml-provider/.*$', self.physical_resource_id): 104 | response = self.iam.delete_saml_provider(SAMLProviderArn=self.physical_resource_id) 105 | 106 | 107 | provider = SAMLProvider() 108 | 109 | 110 | def handler(request, context): 111 | return provider.handle(request, context) 112 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | exceptiongroup==1.2.2; python_version < '3.11' 3 | iniconfig==2.0.0; python_version >= '3.7' 4 | packaging==24.2; python_version >= '3.8' 5 | pluggy==1.5.0; python_version >= '3.8' 6 | pytest==8.3.3; python_version >= '3.8' 7 | tomli==2.0.2; python_version < '3.11' 8 | -------------------------------------------------------------------------------- /tests/test_saml_provider.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | from saml_provider import handler 8 | 9 | metadata = ''' 10 | 11 | 12 | 13 | 14 | MIICsjCCAZqgAwIBAgIJaqWEM+WWXxFJMA0GCSqGSIb3DQEBBQUAMAAwHhcNMTYwMTA0MTQzNjQ3WhcNMjkwOTEyMTQzNjQ3WjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtPbIvSsliCGVS3g++KcXwzAATBtHT5YN1bUbisW7n16Ri0IzXLWCyeGdEtvzMr78aCQQ5IfRx1LGJc3x4JTFRJY5x5vpQrkUfmpVdly3sPmpD4oo4Q6qQy8WDOmskplHRevbkVj6QXFp571JpNyzq6+s22Kjp01btA17OhJEBrrFlIYZIDfH/zm4EEABL5UBSpXCJEjkxBkXbmPRbyyfGWxX4vDVNFar7J8PdOg4D7Wvhunug8mWeMIHurs+ZzwEc/CW1yqE2d/J9SG7jJD5dXLeKZeasjtPDDbHqoNc4qk9kfc2pWiPByYG/1wx3QXY/0of0ENUvqg3lLcTSmYetwIDAQABoy8wLTAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBSHkjmCxyhXEn4IZX8PltaoZtN5VTANBgkqhkiG9w0BAQUFAAOCAQEAD9bcO+YYz8bluf6kx3k3MsnsbD5Vat2Q49l46mdMI+gfqI9Q0iHOSVLwYoPOHqO2a3wuNVyz5kTsAJfPWaZpuiTZt+bkp4ZUQU5bsdtNQfZOvIhI8sIvI+bYKGgU3Q4DH2HslubH3Wrw07rFHIbsRlf2eOhPgH4E9cOONNgivwCyT5YlNAbk5ZdRvFvtigX5O3voMiVZfU+B+RPPMe7owYIboDEPy9murdoaF2GnoP3TMDtkAsdbd/DvsHtHyKOfSWTr6Ls7x3wWJ6+T7YO+qpyMm5guffOfX4kahyi2akOdIb806CbZ+WGnKRsSvx0u9Ps3PkNNxjVDcNJqmeXWgQ== 15 | 16 | 17 | 18 | 19 | 20 | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress 21 | urn:oasis:names:tc:SAML:2.0:nameid-format:persistent 22 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ''' 33 | 34 | 35 | def test_create_with_url(): 36 | # create with url 37 | name = 'n%s' % uuid.uuid4() 38 | request = Request('Create', name, 'https://mvanholsteijn.eu.auth0.com/samlp/metadata/44dxGEx0lDr1cpPzBwr7jv9IyWcHGSiY') 39 | response = handler(request, {}) 40 | assert response['Status'] == 'SUCCESS', response['Reason'] 41 | assert 'PhysicalResourceId' in response 42 | physical_resource_id = response['PhysicalResourceId'] 43 | 44 | # update 45 | request = Request('Update', name, metadata, physical_resource_id) 46 | response = handler(request, {}) 47 | assert response['Status'] == 'SUCCESS', response['Reason'] 48 | assert 'PhysicalResourceId' in response 49 | 50 | # delete 51 | request = Request('Delete', {}, physical_resource_id) 52 | response = handler(request, {}) 53 | assert response['Status'] == 'SUCCESS', response['Reason'] 54 | 55 | 56 | def test_create_with_bad_url(): 57 | name = 'n%s' % uuid.uuid4() 58 | request = Request('Create', name, 'http://does-not-exist') 59 | response = handler(request, {}) 60 | assert response['Status'] == 'FAILED', response['Reason'] 61 | assert 'PhysicalResourceId' in response 62 | assert response['PhysicalResourceId'] == 'could-not-create' 63 | 64 | 65 | def test_create_with_bad_metadata(): 66 | name = 'n%s' % uuid.uuid4() 67 | request = Request('Create', name, '' + (' ' * 1000) + '') 68 | response = handler(request, {}) 69 | assert response['Status'] == 'FAILED', response['Reason'] 70 | assert 'PhysicalResourceId' in response 71 | assert response['PhysicalResourceId'] == 'could-not-create' 72 | 73 | 74 | def test_create_with_metadata(): 75 | # create with metadata 76 | name = 'n%s' % uuid.uuid4() 77 | request = Request('Create', name, metadata) 78 | response = handler(request, {}) 79 | assert response['Status'] == 'SUCCESS', response['Reason'] 80 | assert 'PhysicalResourceId' in response 81 | physical_resource_id = response['PhysicalResourceId'] 82 | 83 | # update 84 | request = Request( 85 | 'Update', name, 'https://mvanholsteijn.eu.auth0.com/samlp/metadata/44dxGEx0lDr1cpPzBwr7jv9IyWcHGSiY', physical_resource_id) 86 | response = handler(request, {}) 87 | assert response['Status'] == 'SUCCESS', response['Reason'] 88 | assert 'PhysicalResourceId' in response 89 | 90 | # delete 91 | request = Request('Delete', {}, physical_resource_id) 92 | response = handler(request, {}) 93 | assert response['Status'] == 'SUCCESS', response['Reason'] 94 | 95 | 96 | def test_delete_incorrect_physical_resource_id(): 97 | request = Request('Delete', {}, 'devapi-consumeradmin-ManageOwnConsumersPermission-Y38O818GY2PV') 98 | response = handler(request, {}) 99 | assert response['Status'] == 'SUCCESS', response['Reason'] 100 | 101 | 102 | class Request(dict): 103 | 104 | def __init__(self, request_type, name, metadata, physical_resource_id=None): 105 | request_id = 'request-%s' % uuid.uuid4() 106 | self.update({ 107 | 'RequestType': request_type, 108 | 'ResponseURL': 'https://httpbin.org/put', 109 | 'StackId': 'arn:aws:cloudformation:us-west-2:EXAMPLE/stack-name/guid', 110 | 'RequestId': request_id, 111 | 'ResourceType': 'Custom::SAMLProvider', 112 | 'LogicalResourceId': 'MyCustom', 113 | 'ResourceProperties': { 114 | 'Name': name 115 | }}) 116 | 117 | if metadata.startswith('http'): 118 | self['ResourceProperties']['URL'] = metadata 119 | else: 120 | self['ResourceProperties']['Metadata'] = metadata 121 | 122 | self['PhysicalResourceId'] = physical_resource_id if physical_resource_id is not None else 'initial-%s' % str(uuid.uuid4()) 123 | --------------------------------------------------------------------------------