├── .VERSION ├── _CI ├── bin │ └── .gitkeep ├── .VERSION ├── patches │ └── .gitkeep ├── files │ ├── logging_level.json │ ├── environment_variables.json │ └── prerequisites.json ├── __init__.py ├── scripts │ ├── __init__.py │ ├── bootstrap.py │ ├── reset.py │ ├── lint.py │ ├── lock.py │ ├── graph.py │ ├── _initialize_template.py │ ├── test.py │ ├── document.py │ ├── upload.py │ ├── build.py │ ├── update.py │ └── tag.py ├── configuration │ ├── __init__.py │ └── configuration.py └── library │ └── __init__.py ├── graphs └── .gitkeep ├── docs ├── authors.rst ├── history.rst ├── readme.rst ├── contributing.rst ├── installation.rst ├── usage.rst ├── index.rst ├── Makefile └── conf.py ├── tests ├── README ├── __init__.py └── test_awsapilib.py ├── setup.cfg ├── .prospector.yaml ├── USAGE_SSO.rst ├── AUTHORS.rst ├── .editorconfig ├── INSTALLATION.rst ├── MANIFEST.in ├── setup_aliases.ps1 ├── tox.ini ├── .gitlab-ci.yml ├── requirements.txt ├── .pylintrc ├── .gitignore ├── Pipfile ├── USAGE_BILLING.rst ├── setup_aliases.sh ├── LICENSE ├── dev-requirements.txt ├── CONTRIBUTING.rst ├── .github └── workflows │ └── main.yml ├── awsapilib ├── awsapilibexceptions.py ├── cloudformation │ ├── cloudformationexceptions.py │ ├── __init__.py │ └── cloudformation.py ├── awsapilib.py ├── sso │ ├── entities │ │ ├── __init__.py │ │ └── entities.py │ ├── __init__.py │ └── ssoexceptions.py ├── captcha │ ├── __init__.py │ ├── captchaexceptions.py │ └── captcha.py ├── authentication │ ├── authenticationexceptions.py │ ├── __init__.py │ └── utils.py ├── controltower │ ├── resources │ │ ├── configuration.py │ │ └── __init__.py │ ├── __init__.py │ └── controltowerexceptions.py ├── billing │ ├── __init__.py │ ├── billingexceptions.py │ └── billing.py ├── _version.py ├── __init__.py └── console │ ├── __init__.py │ └── consoleexceptions.py ├── setup.py ├── README.rst ├── USAGE_CONTROL_TOWER.rst ├── USAGE_ACCOUNT_MANAGER.rst └── HISTORY.rst /.VERSION: -------------------------------------------------------------------------------- 1 | 3.1.5 -------------------------------------------------------------------------------- /_CI/bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /graphs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_CI/.VERSION: -------------------------------------------------------------------------------- 1 | 0.0.0 2 | -------------------------------------------------------------------------------- /_CI/patches/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../INSTALLATION.rst 2 | -------------------------------------------------------------------------------- /_CI/files/logging_level.json: -------------------------------------------------------------------------------- 1 | { 2 | "level": "info" 3 | } 4 | -------------------------------------------------------------------------------- /tests/README: -------------------------------------------------------------------------------- 1 | Please place testing code here. The name should be: test_.py 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [nosetests] 5 | verbosity = 3 6 | with-coverage=1 7 | cover-package=awsapilib/ 8 | cover-erase=1 9 | -------------------------------------------------------------------------------- /.prospector.yaml: -------------------------------------------------------------------------------- 1 | pep257: 2 | disable: 3 | - D203 4 | - D212 5 | - D107 6 | - D105 7 | - D213 8 | - D406 9 | - D407 10 | ignore-paths: 11 | - _CI 12 | - build 13 | - docs 14 | -------------------------------------------------------------------------------- /_CI/files/environment_variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "PIPENV_VENV_IN_PROJECT": "true", 3 | "PIPENV_DEFAULT_PYTHON_VERSION": "3.9", 4 | "PYPI_URL": "https://upload.pypi.org/legacy/", 5 | "PROJECT_SLUG": "awsapilib" 6 | } 7 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | .. include:: ../USAGE_CONTROL_TOWER.rst 6 | 7 | .. include:: ../USAGE_SSO.rst 8 | 9 | .. include:: ../USAGE_BILLING.rst 10 | 11 | .. include:: ../USAGE_ACCOUNT_MANAGER.rst 12 | -------------------------------------------------------------------------------- /_CI/files/prerequisites.json: -------------------------------------------------------------------------------- 1 | { 2 | "executables": [ 3 | "pipenv", 4 | "make" 5 | ], 6 | "environment_variables": [ 7 | ], 8 | "upload_environment_variables": [ 9 | "PYPI_UPLOAD_USERNAME", 10 | "PYPI_UPLOAD_PASSWORD", 11 | "PYPI_URL" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /USAGE_SSO.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Usage for Sso 3 | ============= 4 | 5 | 6 | To use Sso in a project: 7 | 8 | .. code-block:: python 9 | 10 | from awsapilib import Sso 11 | sso = Sso('arn:aws:iam::ACCOUNTID:role/ValidAdministrativeRole') 12 | 13 | for group in sso.groups: 14 | print(group.name) 15 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Costas Tyfoxylos 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Sjoerd Tromp 14 | * Sayantan Khanra 15 | * Soenke Ruempler 16 | * Rafael Zamana Kineippe 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /INSTALLATION.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | $ pip install awsapilib 8 | 9 | Or, if you have virtualenvwrapper installed:: 10 | 11 | $ mkvirtualenv awsapilib 12 | $ pip install awsapilib 13 | 14 | Or, if you are using pipenv:: 15 | 16 | $ pipenv install awsapilib 17 | 18 | Or, if you are using pipx:: 19 | 20 | $ pipx install awsapilib 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include .VERSION 2 | include AUTHORS.rst 3 | include CONTRIBUTING.rst 4 | include HISTORY.rst 5 | include LICENSE 6 | include README.rst 7 | include USAGE.rst 8 | include Pipfile 9 | include Pipfile.lock 10 | include requirements.txt 11 | include dev-requirements.txt 12 | include awsapilib/.VERSION 13 | 14 | recursive-exclude * __pycache__ 15 | recursive-exclude * *.py[co] 16 | 17 | recursive-include docs *.rst conf.py Makefile 18 | recursive-include awsapilib * 19 | -------------------------------------------------------------------------------- /setup_aliases.ps1: -------------------------------------------------------------------------------- 1 | $commands = $(Get-ChildItem ./_CI/scripts -Exclude "_*" |select Name |% {$_.name.split('.')[0]}) 2 | 3 | function New-Alias{ 4 | param ( 5 | $command 6 | ) 7 | $Path="_CI/scripts/$command.py" 8 | $CommandText = 'function _'+$command+'() { if (Test-Path '+$path+') {python '+$path+' $args} else{write-host "executable not found at"'+$path+' -ForegroundColor Red} }' 9 | 10 | Write-Output $CommandText 11 | } 12 | 13 | foreach ($command in $commands) { 14 | Invoke-Expression(New-Alias($command)) 15 | } 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. awsapilib documentation master file, created by 2 | sphinx-quickstart on 2021-04-26. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to awsapilib's documentation! 7 | ===================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | readme 15 | installation 16 | usage 17 | contributing 18 | modules 19 | authors 20 | history 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py39, 8 | 9 | [testenv] 10 | allowlist_externals = * 11 | commands = ./setup.py nosetests --with-coverage --cover-tests --cover-html --cover-html-dir=test-output/coverage --with-html --html-file test-output/nosetests.html 12 | deps = 13 | -rrequirements.txt 14 | -rdev-requirements.txt 15 | passenv = http_proxy,HTTP_PROXY,https_proxy,HTTPS_PROXY,no_proxy,NO_PROXY 16 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - lint 3 | - test 4 | - build 5 | - upload 6 | 7 | lint: 8 | tags: [docker] 9 | stage: lint 10 | image: IMAGE_WITH_PYTHON37_AND_PIPENV 11 | script: _CI/scripts/lint.py 12 | 13 | test: 14 | tags: [docker] 15 | stage: test 16 | image: IMAGE_WITH_PYTHON37_AND_PIPENV 17 | script: _CI/scripts/test.py 18 | 19 | build: 20 | tags: [docker] 21 | stage: build 22 | image: IMAGE_WITH_PYTHON37_AND_PIPENV 23 | script: _CI/scripts/build.py 24 | 25 | upload: 26 | tags: [docker] 27 | stage: upload 28 | image: IMAGE_WITH_PYTHON37_AND_PIPENV 29 | only: 30 | - tags 31 | except: 32 | - branches 33 | script: _CI/scripts/upload.py 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Please do not manually update this file since the requirements are managed 3 | # by pipenv through Pipfile and Pipfile.lock . 4 | # 5 | # This file is created and managed automatically by the template and it is left 6 | # here only for backwards compatibility reasons with python's ecosystem. 7 | # 8 | # Please use Pipfile to update the requirements. 9 | # 10 | requests>=2.31.0 ; python_version >= '3.7' 11 | boto3>=1.34.79 ; python_version >= '3.8' 12 | beautifulsoup4>=4.12.3 ; python_full_version >= '3.6.0' 13 | opnieuw>=1.2.1 ; python_version >= '3.7' 14 | boto3-type-annotations>=0.3.1 15 | cachetools>=4.2.4 ; python_version ~= '3.5' 16 | pyotp>=2.9.0 ; python_version >= '3.7' 17 | 2captcha-python>=1.2.4 ; python_version >= '3.6' -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS .git .hg 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | 28 | [FORMAT] 29 | max-line-length=121 30 | disable=locally-disabled, locally-enabled, logging-format-interpolation, logging-fstring-interpolation 31 | 32 | [General] 33 | init-hook='import sys; sys.path.append("awsapilib")' 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Packages 2 | *.egg 3 | *.egg-info 4 | /dist/ 5 | /build/ 6 | /_build/ 7 | .eggs/ 8 | /eggs/ 9 | /parts/ 10 | /var/ 11 | /sdist/ 12 | /develop-eggs/ 13 | .installed.cfg 14 | /lib/ 15 | /lib64/ 16 | 17 | *.py[cod] 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | test-output 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # Complexity 41 | output/*.html 42 | output/*/index.html 43 | 44 | # Sphinx 45 | docs/_build 46 | 47 | # pyCharm 48 | .idea 49 | 50 | # VirtualEnv 51 | env 52 | .venv 53 | 54 | # Mac 55 | .DS_Store 56 | 57 | # Variables file 58 | .env 59 | 60 | # pyenv local python version marker 61 | .python-version 62 | 63 | # Visual studio code 64 | .vscode 65 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | 8 | sphinx = ">=6.0,<7.0" 9 | sphinx-rtd-theme = ">=1.0,<2.0" 10 | prospector = ">=1.8,<2.0" 11 | coverage = ">=7,<8.0" 12 | nose = ">=1.3,<2.0" 13 | nose-htmloutput = ">=0.1,<1.0" 14 | tox = ">=4.0<5.0" 15 | betamax = ">=0.8,<1.0" 16 | betamax-serializers = "~=0.2,<1.0" 17 | semver = ">=2.0,<3.0" 18 | gitwrapperlib = ">=1.0,<2.0" 19 | twine = ">=4.0,<5.0" 20 | coloredlogs = ">=15.0,<16.0" 21 | emoji = ">=2.0,<3.0" 22 | toml = ">=0.1,<1.0" 23 | boto3-type-annotations-with-docs = ">=0.3,<1.0" 24 | 25 | [packages] 26 | 27 | requests = ">=2.26.0,<3.0" 28 | boto3 = ">=1.24.52,<2.0" 29 | beautifulsoup4 = ">=4.10.0,<5.0" 30 | opnieuw = ">=1.1.0,<2.0" 31 | boto3-type-annotations = ">=0.3.1,<1.0" 32 | cachetools = ">=4.2.4,<5.0" 33 | pyotp = ">=2.6.0,<3.0" 34 | 2captcha-python = ">=1.0.3,<2.0" 35 | -------------------------------------------------------------------------------- /USAGE_BILLING.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Usage for Billing 3 | ================= 4 | 5 | 6 | To use Billing in a project: 7 | 8 | .. code-block:: python 9 | 10 | from awsapilib import Billing 11 | billing = Billing('arn:aws:iam::ACCOUNTID:role/ValidAdministrativeRole') 12 | 13 | # Set tax inheritance on 14 | billing.tax.inheritance = True 15 | 16 | # Set tax information 17 | billing.tax.set_information('some address', 'some city', 'some postal code', 'legal name', 'VAT', 'country code') 18 | 19 | # Enable pdf invoice 20 | billing.preferences.pdf_invoice_by_mail = True 21 | 22 | # Enable credit sharing 23 | billing.preferences.credit_sharing = True 24 | 25 | # Set currency to EUR 26 | billing.currency = 'EUR' 27 | 28 | # Disable IAM access to billing (needs to be enabled by root and cannot be enabled by this after disabled!) 29 | billing.iam_access = False 30 | -------------------------------------------------------------------------------- /setup_aliases.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Needs to be sourced 4 | # Sets up alias functions for the interface while keeping backwards compatibility with the old bash type 5 | 6 | for command in $(ls _CI/scripts/ | cut -d'.' -f1 | grep -v "^_") 7 | do 8 | eval "_$command() { if [ -f _CI/scripts/$command.py ]; then ./_CI/scripts/$command.py \"\$@\"; elif [ -f _CI/scripts/$command ]; then ./_CI/scripts/$command \"\$@\" ;else echo \"Command ./_CI/scripts/$command.py or ./_CI/scripts/$command not found\" ; fi }" 9 | done 10 | 11 | function _activate() { 12 | EXIT_CODE=false 13 | for path_ in '.venv/bin/activate' '_CI/files/.venv/bin/activate' 14 | do 15 | if [ -f "${path_}" ]; then 16 | . "${path_}" 17 | EXIT_CODE=true 18 | break 19 | fi 20 | done 21 | if [ "${EXIT_CODE}" = false ]; then 22 | echo Could not find virtual environment to activate 23 | fi 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Costas Tyfoxylos 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /_CI/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | -------------------------------------------------------------------------------- /_CI/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Please do not manually update this file since the requirements are managed 3 | # by pipenv through Pipfile and Pipfile.lock . 4 | # 5 | # This file is created and managed automatically by the template and it is left 6 | # here only for backwards compatibility reasons with python's ecosystem. 7 | # 8 | # Please use Pipfile to update the requirements. 9 | # 10 | sphinx>=6.2.1 ; python_version >= '3.8' 11 | sphinx-rtd-theme>=1.3.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' 12 | prospector>=1.9.0 ; python_version < '4.0' and python_full_version >= '3.7.2' 13 | coverage>=7.4.4 ; python_version >= '3.8' 14 | nose>=1.3.7 15 | nose-htmloutput>=0.6.0 16 | tox>=4.0.0b2 ; python_version >= '3.7' 17 | betamax>=0.9.0 ; python_full_version >= '3.8.1' 18 | betamax-serializers~=0.2.1 19 | semver>=2.13.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 20 | gitwrapperlib>=1.0.4 21 | twine>=4.0.2 ; python_version >= '3.7' 22 | coloredlogs>=15.0.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' 23 | emoji>=2.11.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 24 | toml>=0.10.2 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' 25 | boto3-type-annotations-with-docs>=0.3.1 -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Submit Feedback 9 | ~~~~~~~~~~~~~~~ 10 | 11 | If you are proposing a feature: 12 | 13 | * Explain in detail how it would work. 14 | * Keep the scope as narrow as possible, to make it easier to implement. 15 | 16 | Get Started! 17 | ------------ 18 | 19 | Ready to contribute? Here's how to set up `awsapilib` for local development. 20 | Using of pipenv is highly recommended. 21 | 22 | 1. Clone your fork locally:: 23 | 24 | $ git clone https://github.com/schubergphilis/awsapilib 25 | 26 | 2. Install your local copy into a virtualenv. Assuming you have pipenv installed, this is how you set up your clone for local development:: 27 | 28 | $ cd awsapilib/ 29 | $ pipenv install --ignore-pipfile 30 | 31 | 3. Create a branch for local development:: 32 | 33 | $ git checkout -b name-of-your-bugfix-or-feature 34 | 35 | Now you can make your changes locally. 36 | Do your development while using the CI capabilities and making sure the code passes lint, test, build and document stages. 37 | 38 | 39 | 4. Commit your changes and push your branch to the server:: 40 | 41 | $ git add . 42 | $ git commit -m "Your detailed description of your changes." 43 | $ git push origin name-of-your-bugfix-or-feature 44 | 45 | 5. Submit a merge request 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ 'main' ] 6 | tags: [ '*' ] 7 | pull_request: 8 | branches: [ 'main' ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: 3.9.15 23 | 24 | - name: Install pipenv 25 | run: pip install pipenv 26 | 27 | - name: Lint 28 | run: _CI/scripts/lint.py 29 | 30 | - name: Test 31 | run: _CI/scripts/test.py 32 | 33 | - name: Build 34 | run: _CI/scripts/build.py 35 | 36 | release: 37 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 38 | needs: build 39 | runs-on: ubuntu-latest 40 | environment: 41 | name: release 42 | steps: 43 | - name: Checkout sources 44 | uses: actions/checkout@v3 45 | 46 | - name: Set up Python 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: 3.9.15 50 | 51 | - name: Install pipenv 52 | run: pip install pipenv 53 | 54 | - name: Upload 55 | run: _CI/scripts/upload.py 56 | 57 | env: 58 | PYPI_UPLOAD_USERNAME: ${{ secrets.PYPI_UPLOAD_USERNAME }} 59 | PYPI_UPLOAD_PASSWORD: ${{ secrets.PYPI_UPLOAD_PASSWORD }} 60 | -------------------------------------------------------------------------------- /_CI/configuration/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | from .configuration import (LOGGING_LEVEL, 26 | ENVIRONMENT_VARIABLES, 27 | PREREQUISITES, 28 | BUILD_REQUIRED_FILES, 29 | LOGGERS_TO_DISABLE, 30 | BRANCHES_SUPPORTED_FOR_TAG, 31 | PROJECT_SLUG) 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | .. _Google Python Style Guide: 28 | http://google.github.io/styleguide/pyguide.html 29 | """ 30 | 31 | __author__ = '''Costas Tyfoxylos ''' 32 | __docformat__ = '''google''' 33 | __date__ = '''26-04-2021''' 34 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 35 | __license__ = '''MIT''' 36 | __maintainer__ = '''Costas Tyfoxylos''' 37 | __email__ = '''''' 38 | __status__ = '''Development''' # "Prototype", "Development", "Production". 39 | -------------------------------------------------------------------------------- /_CI/scripts/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: bootstrap.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import logging 28 | 29 | # this sets up everything and MUST be included before any third party module in every step 30 | import _initialize_template 31 | 32 | from configuration import LOGGING_LEVEL 33 | from library import setup_logging 34 | 35 | # This is the main prefix used for logging 36 | LOGGER_BASENAME = '''_CI.bootstrap''' 37 | LOGGER = logging.getLogger(LOGGER_BASENAME) 38 | LOGGER.addHandler(logging.NullHandler()) 39 | 40 | 41 | def bootstrap(): 42 | setup_logging(os.environ.get("LOGGING_LEVEL") or LOGGING_LEVEL) 43 | 44 | 45 | if __name__ == '__main__': 46 | bootstrap() 47 | -------------------------------------------------------------------------------- /awsapilib/awsapilibexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: awsapilibexceptions.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for awsapilib. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''26-04-2021''' 37 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 38 | __credits__ = ["Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Costas Tyfoxylos''' 41 | __email__ = '''''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | -------------------------------------------------------------------------------- /awsapilib/cloudformation/cloudformationexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: cloudformationexceptions.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for cloudformation. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''13-09-2021''' 37 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 38 | __credits__ = ["Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Costas Tyfoxylos''' 41 | __email__ = '''''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | 44 | 45 | class ServerError(Exception): 46 | """The response was not successful.""" 47 | -------------------------------------------------------------------------------- /_CI/library/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | from .core_library import (activate_template, 27 | execute_command, 28 | setup_logging, 29 | get_project_root_path, 30 | validate_binary_prerequisites, 31 | validate_environment_variable_prerequisites, 32 | is_venv_created, 33 | load_environment_variables, 34 | load_dot_env_file, 35 | clean_up, 36 | save_requirements, 37 | open_file, 38 | bump, 39 | activate_virtual_environment, 40 | tempdir, 41 | update_pipfile) 42 | -------------------------------------------------------------------------------- /awsapilib/awsapilib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: awsapilib.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Main code for awsapilib. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | import logging 35 | 36 | __author__ = '''Costas Tyfoxylos ''' 37 | __docformat__ = '''google''' 38 | __date__ = '''26-04-2021''' 39 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 40 | __credits__ = ["Costas Tyfoxylos"] 41 | __license__ = '''MIT''' 42 | __maintainer__ = '''Costas Tyfoxylos''' 43 | __email__ = '''''' 44 | __status__ = '''Development''' # "Prototype", "Development", "Production". 45 | 46 | 47 | # This is the main prefix used for logging 48 | LOGGER_BASENAME = '''awsapilib''' 49 | LOGGER = logging.getLogger(LOGGER_BASENAME) 50 | LOGGER.addHandler(logging.NullHandler()) 51 | -------------------------------------------------------------------------------- /awsapilib/cloudformation/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | billing package. 28 | 29 | Import all parts from billing here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | from .cloudformation import Cloudformation 35 | from .cloudformationexceptions import ServerError 36 | 37 | __author__ = '''Costas Tyfoxylos ''' 38 | __docformat__ = '''google''' 39 | __date__ = '''30-03-2021''' 40 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 41 | __license__ = '''MIT''' 42 | __maintainer__ = '''Costas Tyfoxylos''' 43 | __email__ = '''''' 44 | __status__ = '''Development''' # "Prototype", "Development", "Production". 45 | 46 | # This is to 'use' the module(s), so lint doesn't complain 47 | 48 | assert Cloudformation 49 | assert ServerError 50 | -------------------------------------------------------------------------------- /_CI/scripts/reset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: reset.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import sys 28 | import shutil 29 | import stat 30 | import logging 31 | 32 | # this sets up everything and MUST be included before any third party module in every step 33 | import _initialize_template 34 | 35 | from configuration import ENVIRONMENT_VARIABLES 36 | from library import clean_up, get_project_root_path 37 | 38 | # This is the main prefix used for logging 39 | LOGGER_BASENAME = '''_CI.reset''' 40 | LOGGER = logging.getLogger(LOGGER_BASENAME) 41 | LOGGER.addHandler(logging.NullHandler()) 42 | 43 | 44 | def reset(environment_variables): 45 | pipfile_path = environment_variables.get('PIPENV_PIPFILE', 'Pipfile') 46 | venv = os.path.join(get_project_root_path(), os.path.dirname(pipfile_path), '.venv') 47 | clean_up(venv) 48 | 49 | 50 | if __name__ == '__main__': 51 | reset(ENVIRONMENT_VARIABLES) 52 | -------------------------------------------------------------------------------- /awsapilib/sso/entities/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Sayantan Khanra 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | entities package. 28 | 29 | Import all parts from entities here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | from .entities import Account, Group, User, PermissionSet 35 | 36 | __author__ = '''Sayantan Khanra , Costas Tyfoxylos ''' 37 | __docformat__ = '''google''' 38 | __date__ = '''18-05-2020''' 39 | __copyright__ = '''Copyright 2020, Sayantan Khanra, Costas Tyfoxylos''' 40 | __credits__ = ["Sayantan Khanra", "Costas Tyfoxylos"] 41 | __license__ = '''MIT''' 42 | __maintainer__ = '''Sayantan Khanra, Costas Tyfoxylos''' 43 | __email__ = ''', ''' 44 | __status__ = '''Development''' # "Prototype", "Development", "Production". 45 | 46 | assert Group 47 | assert User 48 | assert Account 49 | assert PermissionSet 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | try: 6 | from pipenv.project import Project 7 | from pipenv.utils import convert_deps_to_pip 8 | 9 | pfile = Project().parsed_pipfile 10 | requirements = convert_deps_to_pip(pfile['packages'], r=False) 11 | test_requirements = convert_deps_to_pip(pfile['dev-packages'], r=False) 12 | except ImportError: 13 | # get the requirements from the requirements.txt 14 | requirements = [line.strip() 15 | for line in open('requirements.txt').readlines() 16 | if line.strip() and not line.startswith('#')] 17 | # get the test requirements from the test_requirements.txt 18 | test_requirements = [line.strip() 19 | for line in 20 | open('dev-requirements.txt').readlines() 21 | if line.strip() and not line.startswith('#')] 22 | 23 | readme = open('README.rst').read() 24 | history = open('HISTORY.rst').read().replace('.. :changelog:', '') 25 | version = open('.VERSION').read() 26 | 27 | 28 | setup( 29 | name='''awsapilib''', 30 | version=version, 31 | description='''A python library that exposes AWS services that are not covered by boto3, through the usage of undocumented APIs.''', 32 | long_description=readme + '\n\n' + history, 33 | author='''Costas Tyfoxylos''', 34 | author_email='''ctyfoxylos@schubergphilis.com''', 35 | url='''https://github.com/schubergphilis/awsapilib''', 36 | packages=find_packages(where='.', exclude=('tests', 'hooks', '_CI*')), 37 | package_dir={'''awsapilib''': 38 | '''awsapilib'''}, 39 | include_package_data=True, 40 | install_requires=requirements, 41 | license='MIT', 42 | zip_safe=False, 43 | keywords='''awsapilib AWS api ''', 44 | classifiers=[ 45 | 'Development Status :: 4 - Beta', 46 | 'Intended Audience :: Developers', 47 | 'License :: OSI Approved :: MIT License', 48 | 'Natural Language :: English', 49 | 'Programming Language :: Python :: 3.9', 50 | ], 51 | test_suite='tests', 52 | tests_require=test_requirements 53 | ) 54 | -------------------------------------------------------------------------------- /awsapilib/captcha/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | captcha package. 28 | 29 | Import all parts from captcha here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | from .captcha import Solver, Iterm, Terminal, Captcha2 35 | from .captchaexceptions import CaptchaError, UnsupportedTerminal, InvalidOrNoBalanceApiToken 36 | 37 | __author__ = '''Costas Tyfoxylos ''' 38 | __docformat__ = '''google''' 39 | __date__ = '''30-06-2021''' 40 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 41 | __license__ = '''MIT''' 42 | __maintainer__ = '''Costas Tyfoxylos''' 43 | __email__ = '''''' 44 | __status__ = '''Development''' # "Prototype", "Development", "Production". 45 | 46 | # This is to 'use' the module(s), so lint doesn't complain 47 | 48 | assert Solver 49 | assert Iterm 50 | assert Terminal 51 | assert CaptchaError 52 | assert UnsupportedTerminal 53 | assert Captcha2 54 | assert InvalidOrNoBalanceApiToken 55 | -------------------------------------------------------------------------------- /awsapilib/authentication/authenticationexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: authenticationexceptions.py 4 | # 5 | # Copyright 2020 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for authentication. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''11-03-2020''' 37 | __copyright__ = '''Copyright 2020, Costas Tyfoxylos''' 38 | __credits__ = ["Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Costas Tyfoxylos''' 41 | __email__ = '''''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | 44 | 45 | class NoSigninTokenReceived(Exception): 46 | """No Signing token was received.""" 47 | 48 | 49 | class InvalidCredentials(Exception): 50 | """No credentials or the credentials provided are not correct.""" 51 | 52 | 53 | class ExpiredCredentials(Exception): 54 | """Credentials used to assume the role has expired.""" 55 | -------------------------------------------------------------------------------- /_CI/scripts/lint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: lint.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | 28 | # this sets up everything and MUST be included before any third party module in every step 29 | import _initialize_template 30 | 31 | from bootstrap import bootstrap 32 | from emoji import emojize 33 | from library import execute_command 34 | 35 | 36 | # This is the main prefix used for logging 37 | LOGGER_BASENAME = '''_CI.lint''' 38 | LOGGER = logging.getLogger(LOGGER_BASENAME) 39 | LOGGER.addHandler(logging.NullHandler()) 40 | 41 | 42 | def lint(): 43 | bootstrap() 44 | success = execute_command('prospector -DFM --no-autodetect') 45 | if success: 46 | LOGGER.info('%s No linting errors found! %s', 47 | emojize(':check_mark_button:'), 48 | emojize(':thumbs_up:')) 49 | else: 50 | LOGGER.error('%s Linting errors found! %s', 51 | emojize(':cross_mark:'), 52 | emojize(':crying_face:')) 53 | raise SystemExit(0 if success else 1) 54 | 55 | 56 | if __name__ == '__main__': 57 | lint() 58 | -------------------------------------------------------------------------------- /awsapilib/captcha/captchaexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: captchaexceptions.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for captcha. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''30-06-2021''' 37 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 38 | __credits__ = ["Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Costas Tyfoxylos''' 41 | __email__ = '''''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | 44 | 45 | class CaptchaError(Exception): 46 | """There was an error retrieving the captcha.""" 47 | 48 | 49 | class UnsupportedTerminal(Exception): 50 | """The terminal executing under is not supported.""" 51 | 52 | 53 | class InvalidOrNoBalanceApiToken(Exception): 54 | """The api token provided either does not provide access or there is no money on the token to be used.""" 55 | -------------------------------------------------------------------------------- /_CI/scripts/lock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: rebuild_pipfile.py 4 | # 5 | # Copyright 2019 Ilija Matoski 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | import argparse 28 | 29 | # this sets up everything and MUST be included before any third party module in every step 30 | import _initialize_template 31 | 32 | from bootstrap import bootstrap 33 | from library import update_pipfile 34 | 35 | # This is the main prefix used for logging 36 | LOGGER_BASENAME = '''_CI.build''' 37 | LOGGER = logging.getLogger(LOGGER_BASENAME) 38 | LOGGER.addHandler(logging.NullHandler()) 39 | 40 | def get_arguments(): 41 | parser = argparse.ArgumentParser(description='Regenerates Pipfile based on Pipfile.lock') 42 | parser.add_argument('--stdout', 43 | help='Output the Pipfile to stdout', 44 | action="store_true", 45 | default=False) 46 | args = parser.parse_args() 47 | return args 48 | 49 | 50 | def execute(): 51 | bootstrap() 52 | args = get_arguments() 53 | return update_pipfile(args.stdout) 54 | 55 | 56 | if __name__ == '__main__': 57 | raise SystemExit(not execute()) 58 | -------------------------------------------------------------------------------- /awsapilib/controltower/resources/configuration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2020 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | configuration module. 28 | 29 | Import all parts from configuration here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | 35 | import logging 36 | 37 | __author__ = '''Costas Tyfoxylos ''' 38 | __docformat__ = '''google''' 39 | __date__ = '''18-02-2020''' 40 | __copyright__ = '''Copyright 2020, Costas Tyfoxylos''' 41 | __license__ = '''MIT''' 42 | __maintainer__ = '''Costas Tyfoxylos''' 43 | __email__ = '''''' 44 | __status__ = '''Development''' # "Prototype", "Development", "Production". 45 | 46 | 47 | LOGGER_BASENAME = '''controltower''' 48 | LOGGER = logging.getLogger(LOGGER_BASENAME) 49 | LOGGER.addHandler(logging.NullHandler()) 50 | 51 | CREATING_ACCOUNT_ERROR_MESSAGE = 'Package is in state CREATING, but must be in state AVAILABLE' 52 | OU_HIERARCHY_DEPTH_SUPPORTED = 5 53 | PROVISIONED_PRODUCTS_UNDER_CHANGE_FILTER = ["status:UNDER_CHANGE type:CONTROL_TOWER_ACCOUNT"] 54 | -------------------------------------------------------------------------------- /awsapilib/billing/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | billing package. 28 | 29 | Import all parts from billing here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | from .billing import Billing 35 | from .billingexceptions import (InvalidCurrency, 36 | NonEditableSetting, 37 | ServerError, 38 | IAMAccessDenied, 39 | InvalidCountryCode) 40 | 41 | __author__ = '''Costas Tyfoxylos ''' 42 | __docformat__ = '''google''' 43 | __date__ = '''30-03-2021''' 44 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 45 | __license__ = '''MIT''' 46 | __maintainer__ = '''Costas Tyfoxylos''' 47 | __email__ = '''''' 48 | __status__ = '''Development''' # "Prototype", "Development", "Production". 49 | 50 | # This is to 'use' the module(s), so lint doesn't complain 51 | 52 | assert Billing 53 | assert InvalidCurrency 54 | assert NonEditableSetting 55 | assert ServerError 56 | assert IAMAccessDenied 57 | assert InvalidCountryCode 58 | -------------------------------------------------------------------------------- /awsapilib/_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: _version.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Manages the version of the package. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | import os 35 | 36 | __author__ = '''Costas Tyfoxylos ''' 37 | __docformat__ = '''google''' 38 | __date__ = '''26-04-2021''' 39 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 40 | __license__ = '''MIT''' 41 | __maintainer__ = '''Costas Tyfoxylos''' 42 | __email__ = '''''' 43 | __status__ = '''Development''' # "Prototype", "Development", "Production". 44 | 45 | VERSION_FILE_PATH = os.path.abspath( 46 | os.path.join( 47 | os.path.dirname(__file__), 48 | '..', 49 | '.VERSION' 50 | ) 51 | ) 52 | 53 | LOCAL_VERSION_FILE_PATH = os.path.abspath( 54 | os.path.join( 55 | os.path.dirname(__file__), 56 | '.VERSION' 57 | ) 58 | ) 59 | 60 | try: 61 | with open(VERSION_FILE_PATH, encoding='utf-8') as f: 62 | __version__ = f.read() 63 | except IOError: 64 | with open(LOCAL_VERSION_FILE_PATH, encoding='utf-8') as f: 65 | __version__ = f.read() 66 | -------------------------------------------------------------------------------- /awsapilib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | awsapilib package. 28 | 29 | Import all parts from awsapilib here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | from ._version import __version__ 35 | from .authentication import LoggerMixin, Authenticator 36 | from .controltower import ControlTower 37 | from .billing import Billing 38 | from .sso import Sso 39 | from .cloudformation import Cloudformation 40 | from .console import AccountManager, PasswordManager 41 | 42 | __author__ = '''Costas Tyfoxylos ''' 43 | __docformat__ = '''google''' 44 | __date__ = '''26-04-2021''' 45 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 46 | __license__ = '''MIT''' 47 | __maintainer__ = '''Costas Tyfoxylos''' 48 | __email__ = '''''' 49 | __status__ = '''Development''' # "Prototype", "Development", "Production". 50 | 51 | # This is to 'use' the module(s), so lint doesn't complain 52 | assert __version__ 53 | 54 | assert LoggerMixin 55 | assert Authenticator 56 | assert ControlTower 57 | assert Billing 58 | assert Sso 59 | assert Cloudformation 60 | assert AccountManager 61 | assert PasswordManager 62 | -------------------------------------------------------------------------------- /awsapilib/billing/billingexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: billingexceptions.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for billing. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''30-03-2021''' 37 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 38 | __credits__ = ["Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Costas Tyfoxylos''' 41 | __email__ = '''''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | 44 | 45 | class NonEditableSetting(Exception): 46 | """The setting is not editable, or time disabled.""" 47 | 48 | 49 | class InvalidCurrency(Exception): 50 | """The currency provided is not a valid value.""" 51 | 52 | 53 | class ServerError(Exception): 54 | """The response was not successful.""" 55 | 56 | 57 | class IAMAccessDenied(Exception): 58 | """IAM User and Role Access to Billing Information on the account console is not set.""" 59 | 60 | 61 | class InvalidCountryCode(Exception): 62 | """The country code provided is not valid.""" 63 | -------------------------------------------------------------------------------- /awsapilib/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2020 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | awsauthenticationlib package. 28 | 29 | Import all parts from awsauthenticationlib here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | from .authentication import (LoggerMixin, 35 | Authenticator, 36 | Urls, 37 | Domains) 38 | from .authenticationexceptions import (InvalidCredentials, 39 | NoSigninTokenReceived, 40 | ExpiredCredentials) 41 | 42 | __author__ = '''Costas Tyfoxylos ''' 43 | __docformat__ = '''google''' 44 | __date__ = '''18-05-2020''' 45 | __copyright__ = '''Copyright 2020, Costas Tyfoxylos''' 46 | __license__ = '''MIT''' 47 | __maintainer__ = '''Costas Tyfoxylos''' 48 | __email__ = '''''' 49 | __status__ = '''Development''' # "Prototype", "Development", "Production". 50 | 51 | # This is to 'use' the module(s), so lint doesn't complain 52 | assert LoggerMixin 53 | assert Authenticator 54 | assert Urls 55 | assert Domains 56 | 57 | assert NoSigninTokenReceived 58 | assert InvalidCredentials 59 | assert ExpiredCredentials 60 | -------------------------------------------------------------------------------- /tests/test_awsapilib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: test_awsapilib.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | test_awsapilib 28 | ---------------------------------- 29 | Tests for `awsapilib` module. 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | 34 | """ 35 | 36 | from betamax.fixtures import unittest 37 | 38 | __author__ = '''Costas Tyfoxylos ''' 39 | __docformat__ = '''google''' 40 | __date__ = '''26-04-2021''' 41 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 42 | __credits__ = ["Costas Tyfoxylos"] 43 | __license__ = '''MIT''' 44 | __maintainer__ = '''Costas Tyfoxylos''' 45 | __email__ = '''''' 46 | __status__ = '''Development''' # "Prototype", "Development", "Production". 47 | 48 | 49 | class TestAwsapilib(unittest.BetamaxTestCase): 50 | 51 | def setUp(self): 52 | """ 53 | Test set up 54 | 55 | This is where you can setup things that you use throughout the tests. This method is called before every test. 56 | """ 57 | pass 58 | 59 | def tearDown(self): 60 | """ 61 | Test tear down 62 | 63 | This is where you should tear down what you've setup in setUp before. This method is called after every test. 64 | """ 65 | pass 66 | -------------------------------------------------------------------------------- /awsapilib/sso/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2020 Sayantan Khanra, Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | sso package. 28 | 29 | Import all parts from sso here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | from .sso import Sso 35 | from .ssoexceptions import (UnsupportedTarget, 36 | NoPermissionSet, 37 | NoAccount, 38 | NoGroup, 39 | NoProfileID, 40 | NoUser) 41 | 42 | __author__ = '''Sayantan Khanra , Costas Tyfoxylos ''' 43 | __docformat__ = '''google''' 44 | __date__ = '''18-05-2020''' 45 | __copyright__ = '''Copyright 2020, Sayantan Khanra, Costas Tyfoxylos''' 46 | __credits__ = ["Sayantan Khanra", "Costas Tyfoxylos"] 47 | __license__ = '''MIT''' 48 | __maintainer__ = '''Sayantan Khanra, Costas Tyfoxylos''' 49 | __email__ = ''', ''' 50 | __status__ = '''Development''' # "Prototype", "Development", "Production". 51 | 52 | # This is to 'use' the module(s), so lint doesn't complain 53 | assert Sso 54 | assert UnsupportedTarget 55 | assert NoPermissionSet 56 | assert NoAccount 57 | assert NoGroup 58 | assert NoProfileID 59 | assert NoUser 60 | -------------------------------------------------------------------------------- /awsapilib/sso/ssoexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: ssoexceptions.py 4 | # 5 | # Copyright 2020 Sayantan Khanra, Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for sso. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Sayantan Khanra , Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''18-05-2020''' 37 | __copyright__ = '''Copyright 2020, Sayantan Khanra, Costas Tyfoxylos''' 38 | __credits__ = ["Sayantan Khanra", "Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Sayantan Khanra, Costas Tyfoxylos''' 41 | __email__ = ''', ''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | 44 | 45 | class UnsupportedTarget(Exception): 46 | """The target call is not supported by the current implementation.""" 47 | 48 | 49 | class NoPermissionSet(Exception): 50 | """The permission set does not exist.""" 51 | 52 | 53 | class NoAccount(Exception): 54 | """The account does not exist.""" 55 | 56 | 57 | class NoGroup(Exception): 58 | """The group does not exist.""" 59 | 60 | 61 | class NoProfileID(Exception): 62 | """The permission set is not associated with the account.""" 63 | 64 | 65 | class NoUser(Exception): 66 | """The user does not exist.""" 67 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | awsapilib 3 | ========= 4 | 5 | A python library that exposes AWS services that are not covered by boto3, through the usage of undocumented APIs. 6 | 7 | 8 | * Documentation: https://awsapilib.readthedocs.org/en/latest 9 | 10 | 11 | Development Workflow 12 | ==================== 13 | 14 | The workflow supports the following steps 15 | 16 | * lint 17 | * test 18 | * build 19 | * document 20 | * upload 21 | * graph 22 | 23 | These actions are supported out of the box by the corresponding scripts under _CI/scripts directory with sane defaults based on best practices. 24 | Sourcing setup_aliases.ps1 for windows powershell or setup_aliases.sh in bash on Mac or Linux will provide with handy aliases for the shell of all those commands prepended with an underscore. 25 | 26 | The bootstrap script creates a .venv directory inside the project directory hosting the virtual environment. It uses pipenv for that. 27 | It is called by all other scripts before they do anything. So one could simple start by calling _lint and that would set up everything before it tried to actually lint the project 28 | 29 | Once the code is ready to be delivered the _tag script should be called accepting one of three arguments, patch, minor, major following the semantic versioning scheme. 30 | So for the initial delivery one would call 31 | 32 | $ _tag --minor 33 | 34 | which would bump the version of the project to 0.1.0 tag it in git and do a push and also ask for the change and automagically update HISTORY.rst with the version and the change provided. 35 | 36 | 37 | So the full workflow after git is initialized is: 38 | 39 | * repeat as necessary (of course it could be test - code - lint :) ) 40 | 41 | * code 42 | * lint 43 | * test 44 | * commit and push 45 | * develop more through the code-lint-test cycle 46 | * tag (with the appropriate argument) 47 | * build 48 | * upload (if you want to host your package in pypi) 49 | * document (of course this could be run at any point) 50 | 51 | 52 | Important Information 53 | ===================== 54 | 55 | This template is based on pipenv. In order to be compatible with requirements.txt so the actual created package can be used by any part of the existing python ecosystem some hacks were needed. 56 | So when building a package out of this **do not** simple call 57 | 58 | $ python setup.py sdist bdist_egg 59 | 60 | **as this will produce an unusable artifact with files missing.** 61 | Instead use the provided build and upload scripts that create all the necessary files in the artifact. 62 | 63 | 64 | 65 | Project Features 66 | ================ 67 | 68 | * Please look into the usage files. 69 | -------------------------------------------------------------------------------- /_CI/scripts/graph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: graph.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import logging 28 | 29 | # this sets up everything and MUST be included before any third party module in every step 30 | import _initialize_template 31 | 32 | from pathlib import Path 33 | from bootstrap import bootstrap 34 | from emoji import emojize 35 | from library import execute_command 36 | from configuration import PROJECT_SLUG 37 | 38 | # This is the main prefix used for logging 39 | LOGGER_BASENAME = '''_CI.graph''' 40 | LOGGER = logging.getLogger(LOGGER_BASENAME) 41 | LOGGER.addHandler(logging.NullHandler()) 42 | 43 | 44 | def graph(): 45 | bootstrap() 46 | os.chdir('graphs') 47 | create_graph_command = ('pyreverse ' 48 | '-o png ' 49 | '-A ' 50 | '-f PUB_ONLY ' 51 | f'-p graphs \"{Path("..", PROJECT_SLUG)}\"') 52 | success = execute_command(create_graph_command) 53 | if success: 54 | LOGGER.info('%s Successfully created graph images %s', 55 | emojize(':check_mark_button:'), 56 | emojize(':thumbs_up:')) 57 | else: 58 | LOGGER.error('%s Errors in creation of graph images found! %s', 59 | emojize(':cross_mark:'), 60 | emojize(':crying_face:')) 61 | raise SystemExit(0 if success else 1) 62 | 63 | 64 | if __name__ == '__main__': 65 | graph() 66 | -------------------------------------------------------------------------------- /_CI/scripts/_initialize_template.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | 5 | 6 | # This is the main prefix used for logging 7 | LOGGER_BASENAME = '''_CI._initialize_template''' 8 | LOGGER = logging.getLogger(LOGGER_BASENAME) 9 | LOGGER.addHandler(logging.NullHandler()) 10 | 11 | 12 | def add_ci_directory_to_path(): 13 | current_file_path = os.path.dirname(os.path.abspath(__file__)) 14 | ci_path = os.path.abspath(os.path.join(current_file_path, '..')) 15 | if ci_path not in sys.path: 16 | sys.path.append(ci_path) 17 | 18 | 19 | def initialize_template_environment(): 20 | from configuration import (LOGGING_LEVEL, 21 | ENVIRONMENT_VARIABLES, 22 | PREREQUISITES) 23 | from library import (setup_logging, 24 | validate_binary_prerequisites, 25 | validate_environment_variable_prerequisites, 26 | is_venv_created, 27 | execute_command, 28 | load_environment_variables, 29 | load_dot_env_file, 30 | activate_virtual_environment) 31 | load_environment_variables(ENVIRONMENT_VARIABLES) 32 | load_dot_env_file() 33 | if not validate_binary_prerequisites(PREREQUISITES.get('executables', [])): 34 | LOGGER.error('Prerequisite binary missing, cannot continue.') 35 | raise SystemExit(1) 36 | if not validate_environment_variable_prerequisites(PREREQUISITES.get('environment_variables', [])): 37 | LOGGER.error('Prerequisite environment variable missing, cannot continue.') 38 | raise SystemExit(1) 39 | 40 | if not is_venv_created(): 41 | LOGGER.debug('Trying to create virtual environment.') 42 | success = execute_command('pipenv install --dev --ignore-pipfile') 43 | if success: 44 | activate_virtual_environment() 45 | from emoji import emojize 46 | LOGGER.info('%s Successfully created virtual environment and loaded it! %s', 47 | emojize(':check_mark_button:'), 48 | emojize(':thumbs_up:')) 49 | else: 50 | LOGGER.error('Creation of virtual environment failed, cannot continue, ' 51 | 'please clean up .venv directory and try again...') 52 | raise SystemExit(1) 53 | setup_logging(os.environ.get('LOGGING_LEVEL') or LOGGING_LEVEL) 54 | 55 | 56 | def bootstrap_template(): 57 | add_ci_directory_to_path() 58 | from library import activate_template 59 | activate_template() 60 | initialize_template_environment() 61 | 62 | 63 | bootstrap_template() 64 | -------------------------------------------------------------------------------- /_CI/scripts/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: test.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | import os 28 | from time import sleep 29 | 30 | # this sets up everything and MUST be included before any third party module in every step 31 | import _initialize_template 32 | 33 | from emoji import emojize 34 | from bootstrap import bootstrap 35 | from library import (open_file, 36 | clean_up, 37 | execute_command, 38 | save_requirements) 39 | 40 | # This is the main prefix used for logging 41 | LOGGER_BASENAME = '''_CI.test''' 42 | LOGGER = logging.getLogger(LOGGER_BASENAME) 43 | LOGGER.addHandler(logging.NullHandler()) 44 | 45 | 46 | def test(): 47 | bootstrap() 48 | clean_up('test-output') 49 | os.mkdir('test-output') 50 | save_requirements() 51 | success = execute_command('tox') 52 | try: 53 | open_file(os.path.join('test-output', 'coverage', 'index.html')) 54 | sleep(0.5) 55 | open_file(os.path.join('test-output', 'nosetests.html')) 56 | except Exception: 57 | LOGGER.warning('Could not execute UI portion. Maybe running headless?') 58 | if success: 59 | LOGGER.info('%s No testing errors found! %s', 60 | emojize(':check_mark_button:'), 61 | emojize(':thumbs_up:')) 62 | else: 63 | LOGGER.error('%s Testing errors found! %s', 64 | emojize(':cross_mark:'), 65 | emojize(':crying_face:')) 66 | raise SystemExit(0 if success else 1) 67 | 68 | 69 | if __name__ == '__main__': 70 | test() 71 | -------------------------------------------------------------------------------- /_CI/scripts/document.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: document.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import logging 28 | import shutil 29 | 30 | # this sets up everything and MUST be included before any third party module in every step 31 | import _initialize_template 32 | 33 | from bootstrap import bootstrap 34 | from emoji import emojize 35 | from library import open_file, clean_up, execute_command 36 | 37 | # This is the main prefix used for logging 38 | LOGGER_BASENAME = '''_CI.document''' 39 | LOGGER = logging.getLogger(LOGGER_BASENAME) 40 | LOGGER.addHandler(logging.NullHandler()) 41 | 42 | 43 | def document(): 44 | bootstrap() 45 | clean_up(('_build', 46 | os.path.join('docs', '_build'), 47 | os.path.join('docs', 'test_docs.rst'), 48 | os.path.join('docs', 'modules.rst'))) 49 | success = execute_command('make -C docs html') 50 | if success: 51 | shutil.move(os.path.join('docs', '_build'), '_build') 52 | try: 53 | open_file(os.path.join('_build', 'html', 'index.html')) 54 | except Exception: 55 | LOGGER.warning('Could not execute UI portion. Maybe running headless?') 56 | LOGGER.info('%s Successfully built documentation %s', 57 | emojize(':check_mark_button:'), 58 | emojize(':thumbs_up:')) 59 | else: 60 | LOGGER.error('%s Documentation creation errors found! %s', 61 | emojize(':cross_mark:'), 62 | emojize(':crying_face:')) 63 | raise SystemExit(0 if success else 1) 64 | 65 | 66 | if __name__ == '__main__': 67 | document() 68 | -------------------------------------------------------------------------------- /awsapilib/controltower/resources/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2020 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | configuration module. 28 | 29 | Import all parts from configuration here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | 35 | from .configuration import (LOGGER, 36 | LOGGER_BASENAME, 37 | CREATING_ACCOUNT_ERROR_MESSAGE, 38 | OU_HIERARCHY_DEPTH_SUPPORTED, 39 | PROVISIONED_PRODUCTS_UNDER_CHANGE_FILTER) 40 | 41 | from .resources import (CoreAccount, 42 | ControlTowerAccount, 43 | ServiceControlPolicy, 44 | ControlTowerOU, 45 | AccountFactory, 46 | OrganizationsOU, 47 | GuardRail, 48 | ResultOU) 49 | 50 | __author__ = '''Costas Tyfoxylos ''' 51 | __docformat__ = '''google''' 52 | __date__ = '''18-02-2020''' 53 | __copyright__ = '''Copyright 2020, Costas Tyfoxylos''' 54 | __license__ = '''MIT''' 55 | __maintainer__ = '''Costas Tyfoxylos''' 56 | __email__ = '''''' 57 | __status__ = '''Development''' # "Prototype", "Development", "Production". 58 | 59 | # This is to 'use' the module(s), so lint doesn't complain 60 | 61 | assert LOGGER 62 | assert LOGGER_BASENAME 63 | assert CREATING_ACCOUNT_ERROR_MESSAGE 64 | 65 | assert CoreAccount 66 | assert ControlTowerAccount 67 | assert ControlTowerOU 68 | assert AccountFactory 69 | assert ServiceControlPolicy 70 | assert GuardRail 71 | assert OrganizationsOU 72 | assert OU_HIERARCHY_DEPTH_SUPPORTED 73 | assert PROVISIONED_PRODUCTS_UNDER_CHANGE_FILTER 74 | assert ResultOU 75 | -------------------------------------------------------------------------------- /_CI/configuration/configuration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: configuration.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | import os 26 | import json 27 | 28 | current_file_path = os.path.dirname(os.path.abspath(__file__)) 29 | files_path = os.path.abspath(os.path.join(current_file_path, '..', 'files')) 30 | 31 | with open(os.path.join(files_path, 'logging_level.json'), 'r') as logging_level_file: 32 | LOGGING_LEVEL = json.loads(logging_level_file.read()).get('level').upper() 33 | logging_level_file.close() 34 | 35 | with open(os.path.join(files_path, 'environment_variables.json'), 'r') as environment_variables_file: 36 | ENVIRONMENT_VARIABLES = json.loads(environment_variables_file.read()) 37 | environment_variables_file.close() 38 | 39 | with open(os.path.join(files_path, 'prerequisites.json'), 'r') as prerequisites_file: 40 | PREREQUISITES = json.loads(prerequisites_file.read()) 41 | prerequisites_file.close() 42 | 43 | BUILD_REQUIRED_FILES = ('.VERSION', 44 | 'LICENSE', 45 | 'AUTHORS.rst', 46 | 'CONTRIBUTING.rst', 47 | 'HISTORY.rst', 48 | 'README.rst', 49 | 'USAGE_BILLING.rst', 50 | 'USAGE_CONTROL_TOWER.rst', 51 | 'USAGE_SSO.rst', 52 | 'Pipfile', 53 | 'Pipfile.lock', 54 | 'requirements.txt', 55 | 'dev-requirements.txt') 56 | 57 | LOGGERS_TO_DISABLE = ['sh.command', 58 | 'sh.command.process', 59 | 'sh.command.process.streamreader', 60 | 'sh.streamreader', 61 | 'sh.stream_bufferer'] 62 | 63 | BRANCHES_SUPPORTED_FOR_TAG = ['main'] 64 | 65 | PROJECT_SLUG = ENVIRONMENT_VARIABLES.get('PROJECT_SLUG') 66 | -------------------------------------------------------------------------------- /_CI/scripts/upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: upload.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | import os 28 | 29 | # this sets up everything and MUST be included before any third party module in every step 30 | import _initialize_template 31 | 32 | from emoji import emojize 33 | from build import build 34 | from library import execute_command, validate_environment_variable_prerequisites 35 | from configuration import PREREQUISITES 36 | 37 | # This is the main prefix used for logging 38 | LOGGER_BASENAME = '''_CI.upload''' 39 | LOGGER = logging.getLogger(LOGGER_BASENAME) 40 | LOGGER.addHandler(logging.NullHandler()) 41 | 42 | 43 | def upload(): 44 | success = build() 45 | if not success: 46 | LOGGER.error('Errors caught on building the artifact, bailing out...') 47 | raise SystemExit(1) 48 | if not validate_environment_variable_prerequisites(PREREQUISITES.get('upload_environment_variables', [])): 49 | LOGGER.error('Prerequisite environment variable for upload missing, cannot continue.') 50 | raise SystemExit(1) 51 | upload_command = ('twine upload dist/* ' 52 | f'-u {os.environ.get("PYPI_UPLOAD_USERNAME")} ' 53 | f'-p {os.environ.get("PYPI_UPLOAD_PASSWORD")} ' 54 | '--skip-existing ' 55 | f'--repository-url {os.environ.get("PYPI_URL")}') 56 | LOGGER.info('Trying to upload built artifact...') 57 | success = execute_command(upload_command) 58 | if success: 59 | LOGGER.info('%s Successfully uploaded artifact! %s', 60 | emojize(':check_mark_button:'), 61 | emojize(':thumbs_up:')) 62 | else: 63 | LOGGER.error('%s Errors found in uploading artifact! %s', 64 | emojize(':cross_mark:'), 65 | emojize(':crying_face:')) 66 | raise SystemExit(0 if success else 1) 67 | 68 | 69 | if __name__ == '__main__': 70 | upload() 71 | -------------------------------------------------------------------------------- /_CI/scripts/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: build.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | import os 28 | import shutil 29 | 30 | # this sets up everything and MUST be included before any third party module in every step 31 | import _initialize_template 32 | 33 | from bootstrap import bootstrap 34 | from emoji import emojize 35 | from configuration import BUILD_REQUIRED_FILES, LOGGING_LEVEL, PROJECT_SLUG 36 | from library import execute_command, clean_up, save_requirements 37 | 38 | # This is the main prefix used for logging 39 | LOGGER_BASENAME = '''_CI.build''' 40 | LOGGER = logging.getLogger(LOGGER_BASENAME) 41 | LOGGER.addHandler(logging.NullHandler()) 42 | 43 | 44 | def build(): 45 | bootstrap() 46 | clean_up(('build', 'dist')) 47 | success = execute_command('pipenv lock') 48 | if success: 49 | LOGGER.info('Successfully created lock file %s %s', 50 | emojize(':check_mark_button:'), 51 | emojize(':thumbs_up:')) 52 | else: 53 | LOGGER.error('%s Errors creating lock file! %s', 54 | emojize(':cross_mark:'), 55 | emojize(':crying_face:')) 56 | raise SystemExit(1) 57 | save_requirements() 58 | for file in BUILD_REQUIRED_FILES: 59 | shutil.copy(file, os.path.join(f'{PROJECT_SLUG}', file)) 60 | success = execute_command('python setup.py sdist') 61 | if success: 62 | LOGGER.info('%s Successfully built artifact %s', 63 | emojize(':white_heavy_check_mark:'), 64 | emojize(':thumbs_up:')) 65 | else: 66 | LOGGER.error('%s Errors building artifact! %s', 67 | emojize(':cross_mark:'), 68 | emojize(':crying_face:')) 69 | clean_up([os.path.join(f'{PROJECT_SLUG}', file) 70 | for file in BUILD_REQUIRED_FILES]) 71 | return True if success else False 72 | 73 | 74 | if __name__ == '__main__': 75 | raise SystemExit(0 if build() else 1) 76 | -------------------------------------------------------------------------------- /awsapilib/console/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | console package. 28 | 29 | Import all parts from console here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | 35 | from .console import AccountManager, PasswordManager 36 | from .consoleexceptions import (NotSolverInstance, 37 | InvalidAuthentication, 38 | ServerError, 39 | UnableToResolveAccount, 40 | UnableToUpdateAccount, 41 | UnableToQueryMFA, 42 | NoMFAProvided, 43 | UnsupportedMFA, 44 | UnableToRequestResetPassword, 45 | UnableToResetPassword, 46 | UnableToCreateVirtualMFA, 47 | UnableToEnableVirtualMFA, 48 | UnableToDisableVirtualMFA, 49 | UnableToGetVirtualMFA, 50 | VirtualMFADeviceExists) 51 | 52 | __author__ = '''Costas Tyfoxylos ''' 53 | __docformat__ = '''google''' 54 | __date__ = '''30-06-2021''' 55 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 56 | __license__ = '''MIT''' 57 | __maintainer__ = '''Costas Tyfoxylos''' 58 | __email__ = '''''' 59 | __status__ = '''Development''' # "Prototype", "Development", "Production". 60 | 61 | # This is to 'use' the module(s), so lint doesn't complain 62 | 63 | assert AccountManager 64 | assert PasswordManager 65 | 66 | assert NotSolverInstance 67 | assert InvalidAuthentication 68 | assert ServerError 69 | assert UnableToResolveAccount 70 | assert UnableToUpdateAccount 71 | assert UnableToQueryMFA 72 | assert NoMFAProvided 73 | assert UnsupportedMFA 74 | assert UnableToRequestResetPassword 75 | assert UnableToResetPassword 76 | assert UnableToCreateVirtualMFA 77 | assert UnableToEnableVirtualMFA 78 | assert UnableToDisableVirtualMFA 79 | assert UnableToGetVirtualMFA 80 | assert VirtualMFADeviceExists 81 | -------------------------------------------------------------------------------- /awsapilib/controltower/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2020 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | controltower module. 28 | 29 | Import all parts from controltower here 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | """ 34 | from .controltowerexceptions import (UnsupportedTarget, 35 | OUCreating, 36 | NoServiceCatalogAccess, 37 | NonExistentSCP, 38 | NoSuspendedOU, 39 | ServiceCallFailed, 40 | ControlTowerBusy, 41 | ControlTowerNotDeployed, 42 | PreDeployValidationFailed, 43 | EmailCheckFailed, 44 | EmailInUse, 45 | UnavailableRegion, 46 | RoleCreationFailure, 47 | NoActiveArtifactRetrieved, 48 | NonExistentOU, 49 | InvalidParentHierarchy) 50 | from .controltower import ControlTower 51 | 52 | __author__ = '''Costas Tyfoxylos ''' 53 | __docformat__ = '''google''' 54 | __date__ = '''18-02-2020''' 55 | __copyright__ = '''Copyright 2020, Costas Tyfoxylos''' 56 | __license__ = '''MIT''' 57 | __maintainer__ = '''Costas Tyfoxylos''' 58 | __email__ = '''''' 59 | __status__ = '''Development''' # "Prototype", "Development", "Production". 60 | 61 | # This is to 'use' the module(s), so lint doesn't complain 62 | assert ControlTower 63 | 64 | assert UnsupportedTarget 65 | assert OUCreating 66 | assert NoServiceCatalogAccess 67 | assert NonExistentSCP 68 | assert NoSuspendedOU 69 | assert ServiceCallFailed 70 | assert ControlTowerBusy 71 | assert ControlTowerNotDeployed 72 | assert PreDeployValidationFailed 73 | assert EmailCheckFailed 74 | assert EmailInUse 75 | assert UnavailableRegion 76 | assert RoleCreationFailure 77 | assert NoActiveArtifactRetrieved 78 | assert NonExistentOU 79 | assert InvalidParentHierarchy 80 | -------------------------------------------------------------------------------- /awsapilib/console/consoleexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: consoleexceptions.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for console. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''30-06-2021''' 37 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 38 | __credits__ = ["Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Costas Tyfoxylos''' 41 | __email__ = '''''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | 44 | 45 | class NotSolverInstance(Exception): 46 | """The object provided was not of Solver type.""" 47 | 48 | 49 | class InvalidAuthentication(Exception): 50 | """The authentication did not succeed.""" 51 | 52 | 53 | class ServerError(Exception): 54 | """Unknown server error occured.""" 55 | 56 | 57 | class UnableToResolveAccount(Exception): 58 | """Unable to resolve the account type.""" 59 | 60 | 61 | class UnableToUpdateAccount(Exception): 62 | """Unable to update the account info.""" 63 | 64 | 65 | class UnableToQueryMFA(Exception): 66 | """Unable to query the account MFA info.""" 67 | 68 | 69 | class NoMFAProvided(Exception): 70 | """The account is MFA provided but no MFA serial was provided.""" 71 | 72 | 73 | class UnsupportedMFA(Exception): 74 | """The MFA enabled is not supported.""" 75 | 76 | 77 | class UnableToRequestResetPassword(Exception): 78 | """The request to reset password did not work.""" 79 | 80 | 81 | class UnableToResetPassword(Exception): 82 | """The reset password request did not work.""" 83 | 84 | 85 | class UnableToCreateVirtualMFA(Exception): 86 | """The attempt to create a virtual mfa failed.""" 87 | 88 | 89 | class UnableToEnableVirtualMFA(Exception): 90 | """The attempt to create a virtual mfa failed.""" 91 | 92 | 93 | class UnableToDisableVirtualMFA(Exception): 94 | """The attempt to disable a virtual mfa failed.""" 95 | 96 | 97 | class UnableToGetVirtualMFA(Exception): 98 | """The attempt to list a virtual mfa failed.""" 99 | 100 | 101 | class VirtualMFADeviceExists(Exception): 102 | """The device already exists.""" 103 | -------------------------------------------------------------------------------- /USAGE_CONTROL_TOWER.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Usage for ControlTower 3 | ====================== 4 | 5 | 6 | To use ControlTower in a project: 7 | 8 | .. code-block:: python 9 | 10 | from awsapilib import ControlTower 11 | tower = ControlTower('arn:aws:iam::ACCOUNTID:role/ValidAdministrativeRole') 12 | 13 | for account in tower.accounts: 14 | print(account.name) 15 | >>> root 16 | Audit 17 | Log archive 18 | 19 | for account in tower.accounts: 20 | print(account.guardrail_compliance_status) 21 | >>> COMPLIANT 22 | COMPLIANT 23 | COMPLIANT 24 | 25 | for ou in tower.organizational_units: 26 | print(ou.name) 27 | >>> Custom 28 | Core 29 | Root 30 | 31 | # Creates an OU under root 32 | tower.create_organizational_unit('TestOU') 33 | >>> True 34 | 35 | # Creates an OU under Workload/Production 36 | # It would raise NonExistentOU exception if the structure does not exist 37 | tower.create_organizational_unit('TestOU', parent_hierarchy=['Workload','Production']) 38 | >>> True 39 | 40 | # Creates an OU under Workload/Production 41 | # It would create the structure if the structure does not exist 42 | tower.create_organizational_unit('TestOU', parent_hierarchy=['Workload','Production'], force_create=True) 43 | >>> True 44 | 45 | # Deletes an OU under Root OU 46 | tower.delete_organizational_unit('TestOU') 47 | >>> True 48 | 49 | # Deletes an OU under Workload/Production 50 | tower.delete_organizational_unit('TestOU', parent_hierarchy=['Workload','Production']) 51 | >>> True 52 | 53 | 54 | # Creates account "account-name" under OU "SomeOu" under Root OU 55 | tower.create_account(account_name='account-name', 56 | account_email='root-email@domain.com', 57 | organizational_unit='SomeOU') 58 | >>> True 59 | 60 | # Creates account "account-name" under OU "SomeOu" under Workload/Production 61 | # It would raise NonExistentOU exception if the structure does not exist 62 | tower.create_account(account_name='account-name', 63 | account_email='root-email@domain.com', 64 | organizational_unit='SomeOU', 65 | parent_hierarchy=['Workload','Production']) 66 | >>> True 67 | 68 | # Creates account "account-name" under OU "SomeOu" under Workload/Production 69 | # It would create the structure if the structure does not exist 70 | tower.create_account(account_name='account-name', 71 | account_email='root-email@domain.com', 72 | organizational_unit='SomeOU', 73 | parent_hierarchy=['Workload','Production'], 74 | force_parent_hierarchy_creation=True) 75 | >>> True 76 | 77 | 78 | # Creates account "account-name" under OU "SomeOu" under Workload/Production 79 | # It would create the structure if the structure does not exist 80 | # Uses all possible attributes. 81 | tower.create_account(account_name='account-name', 82 | account_email='root-email@domain.com', 83 | organizational_unit='SomeOU', 84 | parent_hierarchy=['Workload','Production'], 85 | product_name='product-name-for-account', 86 | sso_first_name='Bob', 87 | sso_last_name='Builder', 88 | sso_user_email='bob-builder@construction.com', 89 | force_parent_hierarchy_creation=True) 90 | >>> True 91 | -------------------------------------------------------------------------------- /USAGE_ACCOUNT_MANAGER.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Usage for Account Manager 3 | ========================= 4 | 5 | 6 | To use Account Manager in a project: 7 | 8 | .. code-block:: python 9 | 10 | from awsapilib import AccountManager, PasswordManager 11 | 12 | password_manager = PasswordManager() 13 | # Most actions require a captcha to be solved to continue. 14 | # The process is interactive and you get a prompt to solve the captcha by following a url with it 15 | # in a standard console or by presenting the captcha in the terminal if you are using iTerm 16 | 17 | # Using the Captcha2 solver would automate the process. 18 | from awsapilib.captcha import Captcha2 19 | solver = Captcha2('API_TOKEN_HERE_FOR_2CAPTCHA_SERVICE') 20 | password_manager = PasswordManager(solver=solver) 21 | 22 | # Request the reset of a password for an account 23 | password_manager.request_password_reset('EMAIL_OF_AN_ACCOUNT') 24 | # The above should trigger a reset email with a reset link 25 | 26 | # Reset the password 27 | password_manager.reset_password('RESET_URL_RECEIVED_BY_EMAIL_HERE', 'PASSWORD_TO_SET') 28 | 29 | account_manager = AccountManager(email, password, region, mfa_serial) 30 | # Most actions require a captcha to be solved to continue. 31 | # The process is interactive and you get a prompt to solve the captcha by following a url with it 32 | # in a standard console or by presenting the captcha in the terminal if you are using iTerm 33 | 34 | # Using the Captcha2 solver would automate the process. 35 | from awsapilib.captcha import Captcha2 36 | solver = Captcha2('API_TOKEN_HERE_FOR_2CAPTCHA_SERVICE') 37 | account_manager = AccountManager(email, password, region, mfa_serial, solver=solver) 38 | 39 | # Enable IAM billing console access for the account 40 | print(account_manager.iam.billing_console_access) 41 | >>> False 42 | 43 | account_manager.iam.billing_console_access = True 44 | print(account_manager.iam.billing_console_access) 45 | >>> True 46 | 47 | # Interface with MFA actions 48 | # Create a virtual MFA 49 | # Warning! Setting an MFA will require re instantiation of the account manager with the new seed 50 | # before more actions can be performed on the account. 51 | # Also due to eventual consistency there might be some time required between setting the MFA and 52 | # being able to use it in which case there might be authentication errors in between if actions are 53 | # performed in sequence. The time is usually less that half a minute. 54 | seed = account_manager.mfa.create_virtual_device() # default name is "root-account-mfa-device" 55 | # can be overridden by passing a name variable 56 | # !! Save the seed somewhere super safe 57 | 58 | # Get the current virtual MFA 59 | device = account_manager.mfa.get_virtual_device() 60 | print(device.serial_number) 61 | arn:aws:iam::ACCOUNTID:mfa/root-account-mfa-device 62 | 63 | # Delete a virtual MFA 64 | account_manager.mfa.delete_virtual_device(device.serial_number) 65 | 66 | 67 | # Update info and terminate account 68 | 69 | # Update name of account 70 | account_manager.update_account_name('NEW_NAME_TO_SET') 71 | 72 | # Update email of the account 73 | # Due to eventual consistency there might be some time required between changing the email and 74 | # being able to use it in which case there might be authentication errors in between if actions are 75 | # performed in sequence. The time is usually less that half a minute. 76 | account_manager.update_account_email('NEW_EMAIL_TO_SET') 77 | 78 | # Terminate an account 79 | account_manager.terminate_account() 80 | -------------------------------------------------------------------------------- /awsapilib/controltower/controltowerexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: controltowerexceptions.py 4 | # 5 | # Copyright 2020 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for controltower. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''18-02-2020''' 37 | __copyright__ = '''Copyright 2020, Costas Tyfoxylos''' 38 | __credits__ = ["Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Costas Tyfoxylos''' 41 | __email__ = '''''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | 44 | 45 | class UnsupportedTarget(Exception): 46 | """The target call is not supported by the current implementation.""" 47 | 48 | 49 | class OUCreating(Exception): 50 | """The organizational unit is still under creation and cannot be used.""" 51 | 52 | 53 | class NoServiceCatalogAccess(Exception): 54 | """There is no access to service catalog.""" 55 | 56 | 57 | class NonExistentSCP(Exception): 58 | """The SCP requested does not exist.""" 59 | 60 | 61 | class NoSuspendedOU(Exception): 62 | """The suspended ou has not been created.""" 63 | 64 | 65 | class ServiceCallFailed(Exception): 66 | """The call to the service has failed.""" 67 | 68 | 69 | class ControlTowerBusy(Exception): 70 | """The control tower is already executing some action.""" 71 | 72 | 73 | class ControlTowerNotDeployed(Exception): 74 | """The control tower is deployed at all.""" 75 | 76 | 77 | class PreDeployValidationFailed(Exception): 78 | """The pre deployment validation failed.""" 79 | 80 | 81 | class EmailCheckFailed(Exception): 82 | """Checking of the email was not possible.""" 83 | 84 | 85 | class EmailInUse(Exception): 86 | """The email provided is already in use and cannot be used to deploy an account.""" 87 | 88 | 89 | class UnavailableRegion(Exception): 90 | """The region or regions provided to control tower to deploy in are not available.""" 91 | 92 | 93 | class RoleCreationFailure(Exception): 94 | """Unable to create the required roles for the deployment of control tower, manual clean up is required.""" 95 | 96 | 97 | class NoActiveArtifactRetrieved(Exception): 98 | """Could not retrieve an active artifact.""" 99 | 100 | 101 | class NonExistentOU(Exception): 102 | """The OU name provided does not exist in Control Tower.""" 103 | 104 | 105 | class InvalidParentHierarchy(Exception): 106 | """The parent hierarchy provided is not valid.""" 107 | -------------------------------------------------------------------------------- /_CI/scripts/update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: update.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import sys 28 | import tempfile 29 | from glob import glob 30 | from dataclasses import dataclass 31 | 32 | # this sets up everything and MUST be included before any third party module in every step 33 | import _initialize_template 34 | 35 | from library import clean_up 36 | from patch import fromfile, setdebug 37 | 38 | 39 | @dataclass() 40 | class Project: 41 | name: str 42 | full_path: str 43 | parent_directory_full_path: str 44 | 45 | 46 | class PatchFailure(Exception): 47 | """The patch process failed""" 48 | 49 | 50 | def get_current_version(): 51 | with open(os.path.join('_CI', '.VERSION'), 'r') as version_file: 52 | version = version_file.read().strip() 53 | version_file.close() 54 | print(f'Got current template version {version}') 55 | return version 56 | 57 | 58 | def apply_patch(file_path, project_parent_path): 59 | patcher = fromfile(file_path) 60 | return patcher.apply(0, project_parent_path) 61 | 62 | 63 | def get_patches_to_apply(current_version): 64 | patches = [] 65 | for patch_file in glob(os.path.join('_CI', 'patches', '*.patch')): 66 | version = patch_file.rpartition(os.path.sep)[2].split('.patch')[0] 67 | if version > current_version: 68 | patches.append(patch_file) 69 | return sorted(patches) 70 | 71 | 72 | def get_interpolated_temp_patch_file(patch_file, project_name): 73 | patch_diff = open(patch_file, 'r').read() 74 | temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False) 75 | temp_file.write(patch_diff.replace('{{cookiecutter.project_slug}}', project_name)) 76 | temp_file.close() 77 | return temp_file.name 78 | 79 | 80 | def initialize(): 81 | current_file_path = os.path.dirname(os.path.abspath(__file__)) 82 | project_root = os.path.abspath(os.path.join(current_file_path, '..', '..')) 83 | project_parent_path, _, parent_directory_name = project_root.rpartition(os.path.sep) 84 | os.chdir(project_root) 85 | sys.path.append(os.path.join(project_root, '_CI/library')) 86 | setdebug() 87 | return Project(parent_directory_name, project_root, project_parent_path) 88 | 89 | 90 | def apply_patches(patches, project): 91 | for diff_patch in patches: 92 | print(f'Interpolating project name "{project.name}" in patch {diff_patch}') 93 | patch_file = get_interpolated_temp_patch_file(diff_patch, project.name) 94 | success = apply_patch(patch_file, project.parent_directory_full_path) 95 | print(f'Removing temporary file "{patch_file}"') 96 | clean_up(patch_file) 97 | if success: 98 | print(f'Successfully applied patch {diff_patch}') 99 | else: 100 | print(f'Failed applying patch {diff_patch}') 101 | raise PatchFailure(diff_patch) 102 | 103 | 104 | if __name__ == '__main__': 105 | project = initialize() 106 | current_version = get_current_version() 107 | patches_to_apply = get_patches_to_apply(current_version) 108 | try: 109 | apply_patches(patches_to_apply, project) 110 | except PatchFailure: 111 | SystemExit(1) 112 | raise SystemExit(0) 113 | -------------------------------------------------------------------------------- /_CI/scripts/tag.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: tag.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import argparse 27 | import logging 28 | 29 | from datetime import datetime 30 | 31 | # this sets up everything and MUST be included before any third party module in every step 32 | import _initialize_template 33 | 34 | from emoji import emojize 35 | from bootstrap import bootstrap 36 | from gitwrapperlib import Git 37 | from library import bump 38 | from configuration import BRANCHES_SUPPORTED_FOR_TAG 39 | 40 | 41 | # This is the main prefix used for logging 42 | LOGGER_BASENAME = '''_CI.tag''' 43 | LOGGER = logging.getLogger(LOGGER_BASENAME) 44 | LOGGER.addHandler(logging.NullHandler()) 45 | 46 | 47 | def check_branch(): 48 | git = Git() 49 | if git.get_current_branch() not in BRANCHES_SUPPORTED_FOR_TAG: 50 | accepted_branches = ', '.join(BRANCHES_SUPPORTED_FOR_TAG) 51 | print("Tagging is only supported on {} " 52 | "you should not tag any other branch, exiting!".format(accepted_branches)) 53 | raise SystemExit(1) 54 | 55 | 56 | def push(current_version): 57 | git = Git() 58 | git.commit('Updated history file with changelog', 'HISTORY.rst') 59 | git.commit(f'Set version to {current_version}', '.VERSION') 60 | git.add_tag(current_version) 61 | git.push() 62 | git.push('origin', current_version) 63 | return current_version 64 | 65 | 66 | def _get_user_input(version): 67 | print(f'Enter/Paste your history changelog for version {version}.\n' 68 | f'Each comment can be a different line.\n\n' 69 | f'Ctrl-D ( Mac | Linux ) or Ctrl-Z ( windows ) to save it.\n') 70 | contents = [] 71 | while True: 72 | try: 73 | line = input() 74 | except EOFError: 75 | break 76 | contents.append(line) 77 | return contents 78 | 79 | 80 | def _get_changelog(contents, version): 81 | header = f'{version} ({datetime.today().strftime("%d-%m-%Y")})' 82 | underline = '-' * len(header) 83 | return (f'\n\n{header}\n' 84 | f'{underline}\n\n* ' + '\n* '.join([line for line in contents if line]) + '\n') 85 | 86 | 87 | def update_history_file(version): 88 | comments = _get_user_input(version) 89 | update_text = _get_changelog(comments, version) 90 | with open('HISTORY.rst', 'a') as history_file: 91 | history_file.write(update_text) 92 | history_file.close() 93 | 94 | 95 | def get_arguments(): 96 | parser = argparse.ArgumentParser(description='Handles bumping of the artifact version') 97 | group = parser.add_mutually_exclusive_group() 98 | group.add_argument('--major', help='Bump the major version', action='store_true') 99 | group.add_argument('--minor', help='Bump the minor version', action='store_true') 100 | group.add_argument('--patch', help='Bump the patch version', action='store_true') 101 | args = parser.parse_args() 102 | return args 103 | 104 | 105 | def tag(): 106 | bootstrap() 107 | args = get_arguments() 108 | check_branch() 109 | if args.major: 110 | version = bump('major') 111 | update_history_file(version) 112 | elif args.minor: 113 | version = bump('minor') 114 | update_history_file(version) 115 | elif args.patch: 116 | version = bump('patch') 117 | update_history_file(version) 118 | else: 119 | version = bump() 120 | print(version) 121 | raise SystemExit(0) 122 | version = push(version) 123 | print(version) 124 | 125 | 126 | if __name__ == '__main__': 127 | tag() 128 | -------------------------------------------------------------------------------- /awsapilib/cloudformation/cloudformation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: cloudformation.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Main code for billing. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | import logging 35 | 36 | from awsapilib.authentication import Authenticator, LoggerMixin 37 | 38 | from .cloudformationexceptions import ServerError 39 | 40 | __author__ = '''Costas Tyfoxylos ''' 41 | __docformat__ = '''google''' 42 | __date__ = '''13-09-2021''' 43 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 44 | __credits__ = ["Costas Tyfoxylos"] 45 | __license__ = '''MIT''' 46 | __maintainer__ = '''Costas Tyfoxylos''' 47 | __email__ = '''''' 48 | __status__ = '''Development''' # "Prototype", "Development", "Production". 49 | 50 | # This is the main prefix used for logging 51 | LOGGER_BASENAME = '''cloudformation''' 52 | LOGGER = logging.getLogger(LOGGER_BASENAME) 53 | LOGGER.addHandler(logging.NullHandler()) 54 | 55 | 56 | class Cloudformation(LoggerMixin): 57 | """Models Control Tower by wrapping around service catalog.""" 58 | 59 | def __init__(self, arn, region=None): 60 | self.aws_authenticator = Authenticator(arn) 61 | self.session = self._get_authenticated_session() 62 | self.region = region or self.aws_authenticator.region 63 | 64 | def _get_authenticated_session(self): 65 | return self.aws_authenticator.get_cloudformation_authenticated_session() 66 | 67 | @property 68 | def stacksets(self): 69 | """Exposes the stacksets settings.""" 70 | return StackSet(self) 71 | 72 | 73 | class StackSet: 74 | """Models the stacksets settings and implements the interaction with them.""" 75 | 76 | def __init__(self, cloudformation_instance): 77 | self._cloudformation = cloudformation_instance 78 | self._api_url = (f'{cloudformation_instance.aws_authenticator.urls.regional_console}/' 79 | f'cloudformation/service/stacksets/') 80 | self._region_payload = {'region': self._cloudformation.aws_authenticator.region} 81 | 82 | @property 83 | def organizations_trusted_access(self): 84 | """Setting about the organizations trusted access.""" 85 | endpoint = 'describeOrganizationsTrustedAccess' 86 | response = self._cloudformation.session.get(f'{self._api_url}/{endpoint}', params=self._region_payload) 87 | if not response.ok: 88 | raise ServerError(f'Error, response received : {response.text}') 89 | return response.json().get('status') == 'ENABLED' 90 | 91 | @organizations_trusted_access.setter 92 | def organizations_trusted_access(self, value): 93 | """Setter of the organizations trusted access.""" 94 | value = bool(value) 95 | return self.enable_organizations_trusted_access() if value else self.disable_organizations_trusted_access() 96 | 97 | def enable_organizations_trusted_access(self): 98 | """Enables organization trusted access. 99 | 100 | Returns: 101 | True on success 102 | 103 | """ 104 | endpoint = 'enableOrganizationsTrustedAccess' 105 | return self._set_organizations_trusted_access(endpoint) 106 | 107 | def disable_organizations_trusted_access(self): 108 | """Disables organization trusted access. 109 | 110 | Returns: 111 | True on success 112 | 113 | """ 114 | endpoint = 'disableOrganizationsTrustedAccess' 115 | return self._set_organizations_trusted_access(endpoint) 116 | 117 | def _set_organizations_trusted_access(self, endpoint): 118 | response = self._cloudformation.session.post(f'{self._api_url}/{endpoint}', 119 | params=self._region_payload, 120 | json={}) 121 | if any([not response.ok, 'Error' in response.json()]): 122 | raise ServerError(f'Error, response received : {response.text}') 123 | return True 124 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.0.1 (26-04-2021) 7 | --------------------- 8 | 9 | * First code creation 10 | 11 | 12 | 0.1.0 (11-05-2021) 13 | ------------------ 14 | 15 | * Initial release 16 | 17 | 18 | 0.1.1 (17-05-2021) 19 | ------------------ 20 | 21 | * Filtering out failed accounts from checking their update status 22 | 23 | 24 | 0.1.2 (17-05-2021) 25 | ------------------ 26 | 27 | * Fixed a timing issue with getting the active service catalog product on account creation. 28 | 29 | 30 | 0.2.0 (18-05-2021) 31 | ------------------ 32 | 33 | * Exposed governed and non governed regions and a small fix with latest update changes. 34 | 35 | 36 | 0.2.1 (18-05-2021) 37 | ------------------ 38 | 39 | * Dynamically retrieving updatable information about control tower. 40 | 41 | 42 | 0.2.2 (19-05-2021) 43 | ------------------ 44 | 45 | * Added some blocking on actions to prevent race conditions. 46 | 47 | 48 | 0.2.3 (08-06-2021) 49 | ------------------ 50 | 51 | * Bumped dependencies. 52 | 53 | 54 | 0.2.4 (16-06-2021) 55 | ------------------ 56 | 57 | * Added new feature to provision instance_id for an account 58 | 59 | 60 | 0.3.0 (16-06-2021) 61 | ------------------ 62 | 63 | * Added new method to provision saml config in the account 64 | 65 | 66 | 0.4.0 (17-06-2021) 67 | ------------------ 68 | 69 | * Added provision_saml_provider to the public api 70 | 71 | 72 | 0.4.1 (19-08-2021) 73 | ------------------ 74 | 75 | * Add explict error handling on bad response. 76 | 77 | 78 | 0.4.2 (01-09-2021) 79 | ------------------ 80 | 81 | * Added pagination on organizational OU retrieval. 82 | 83 | 84 | 0.5.0 (09-09-2021) 85 | ------------------ 86 | 87 | * Explicitly passing region to control tower instantiation. 88 | 89 | 90 | 0.5.1 (09-09-2021) 91 | ------------------ 92 | 93 | * Raising exception if csrf token retrieved has no value. 94 | 95 | 96 | 0.5.2 (09-09-2021) 97 | ------------------ 98 | 99 | * Fixed hardcoded url pointing to eu-west-1 making it possible to deploy to other home regions than Ireland. 100 | 101 | 102 | 0.6.0 (01-10-2021) 103 | ------------------ 104 | 105 | * Implemented contol tower repair and bumped dependencies. 106 | 107 | 108 | 0.7.0 (14-10-2021) 109 | ------------------ 110 | 111 | * - Adding a force option to the register_ou function to force re-registering 112 | 113 | 114 | 0.8.0 (14-10-2021) 115 | ------------------ 116 | 117 | * - Adding a force option to the register_ou function to force re-registering 118 | 119 | 120 | 0.9.0 (18-10-2021) 121 | ------------------ 122 | 123 | * - Adding support to also show updated state when the landingzone gets a new configuration 124 | 125 | 126 | 0.10.0 (29-11-2021) 127 | ------------------- 128 | 129 | * Implemented cloudformation stack set organizations trusted access enabling and disabling. 130 | 131 | 132 | 0.10.1 (29-11-2021) 133 | ------------------- 134 | 135 | * Added missing dependencies. 136 | 137 | 138 | 1.0.0 (03-12-2021) 139 | ------------------ 140 | 141 | * Implemented account lifecycle and info update, MFA support and IAM billing console enablement. 142 | 143 | 144 | 1.1.0 (18-12-2021) 145 | ------------------ 146 | 147 | * Added support for nested OUs 148 | 149 | 150 | 2.0.0 (28-12-2021) 151 | ------------------ 152 | 153 | * Releasing support for 5 levels of nested OUs 154 | 155 | 156 | 2.0.1 (29-12-2021) 157 | ------------------ 158 | 159 | * Fixed a bug where on account creation the OU was not created unless there was a failure with the parent hierarchy. 160 | 161 | 162 | 2.0.2 (29-12-2021) 163 | ------------------ 164 | 165 | * Fixed a bug with the handling of the OU without hierarchies. 166 | 167 | 168 | 2.1.0 (30-12-2021) 169 | ------------------ 170 | 171 | * Implemented capability of retrieving account by email. 172 | 173 | 174 | 2.1.1 (22-02-2022) 175 | ------------------ 176 | 177 | * Updated 'get_changing_accounts' to use the search_provisioned_products with a filter which will fix the bug where only the first 100 provisioned products were checked for status "UNDER_CHAGE". 178 | 179 | 180 | 2.2.0 (26-04-2022) 181 | ------------------ 182 | 183 | * Fix for new console authentication flow courtesy of Soenke Ruempler , author of the awesome superwerker! 184 | 185 | 186 | 2.3.0 (05-05-2022) 187 | ------------------ 188 | 189 | * Fix for IAM authentication flow. 190 | 191 | 192 | 2.3.1 (18-05-2022) 193 | ------------------ 194 | 195 | * Fix for cases where captcha is actually not required. 196 | 197 | 198 | 2.3.2 (23-05-2022) 199 | ------------------ 200 | 201 | * Another captcha fix for the root console curtesy of Rafael Zamana Kineippe 202 | 203 | 204 | 3.0.0 (13-06-2022) 205 | ------------------ 206 | 207 | * awsapilib calling aws native apis rather than shadow apis 208 | 209 | 210 | 3.1.0 (17-06-2022) 211 | ------------------ 212 | 213 | * fix control tower deploy/setup 214 | 215 | 216 | 3.1.1 (17-08-2022) 217 | ------------------ 218 | 219 | * Bumped dependencies. 220 | 221 | 222 | 3.1.2 (27-09-2022) 223 | ------------------ 224 | 225 | * Fix for support for Control Tower update for versions over 2.6. 226 | 227 | 228 | 3.1.3 (03-03-2023) 229 | ------------------ 230 | 231 | * Bump and loosen dependencies. 232 | 233 | 234 | 3.1.4 (29-11-2023) 235 | ------------------ 236 | 237 | * Remove gov regions from Control Tower. Bump dependencies. 238 | 239 | 240 | 3.1.5 (08-04-2024) 241 | ------------------ 242 | 243 | * Fix billing api changes. 244 | -------------------------------------------------------------------------------- /awsapilib/captcha/captcha.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: captcha.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Main code for captcha. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | import base64 35 | import logging 36 | import os 37 | from abc import ABC, abstractmethod 38 | 39 | import requests 40 | from twocaptcha import TwoCaptcha, ValidationException, TimeoutException 41 | from twocaptcha.api import ApiException, NetworkException 42 | 43 | from awsapilib.authentication import LoggerMixin 44 | from .captchaexceptions import CaptchaError, UnsupportedTerminal, InvalidOrNoBalanceApiToken 45 | 46 | __author__ = '''Costas Tyfoxylos ''' 47 | __docformat__ = '''google''' 48 | __date__ = '''30-06-2021''' 49 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 50 | __credits__ = ["Costas Tyfoxylos"] 51 | __license__ = '''MIT''' 52 | __maintainer__ = '''Costas Tyfoxylos''' 53 | __email__ = '''''' 54 | __status__ = '''Development''' # "Prototype", "Development", "Production". 55 | 56 | # This is the main prefix used for logging 57 | LOGGER_BASENAME = '''captcha''' 58 | LOGGER = logging.getLogger(LOGGER_BASENAME) 59 | LOGGER.addHandler(logging.NullHandler()) 60 | 61 | 62 | class Solver(ABC, LoggerMixin): 63 | """Interface for a Solver object.""" 64 | 65 | def __call__(self, *args, **kwargs): 66 | return self 67 | 68 | @abstractmethod 69 | def solve(self, url): 70 | """Solves a url.""" 71 | 72 | 73 | class Iterm(Solver): 74 | """Interactive captcha solver for iTerm terminals.""" 75 | 76 | def __init__(self): 77 | terminal = os.environ.get('TERM_PROGRAM', 'UNKNOWN') 78 | if 'iterm' not in terminal.lower(): 79 | raise UnsupportedTerminal(terminal) 80 | 81 | def solve(self, url): 82 | """Presents a captcha image and returns the user's guess for the captcha. 83 | 84 | Args: 85 | url (str): The url to provide that should have the captcha image. 86 | 87 | Returns: 88 | guess (str): The guess of the user for the captcha. 89 | 90 | """ 91 | response = requests.get(url, timeout=5) 92 | if not response.ok: 93 | raise CaptchaError(response.text) 94 | image = base64.b64encode(response.content).decode() 95 | print(f'\033]1337;File=inline=1;width=400px;height=140px:{image}\a\n') 96 | try: 97 | guess = input('Captcha: ') 98 | except KeyboardInterrupt: 99 | raise CaptchaError(f'User interrupted.\nIf the captcha was not showing correctly please check that the url' 100 | f'{url} indeed points to a valid captcha image..') from None 101 | return guess 102 | 103 | 104 | class Terminal(Solver): 105 | """Interactive captcha solver for standard terminals.""" 106 | 107 | def solve(self, url): 108 | """Presents a captcha image url and returns the user's guess for the captcha. 109 | 110 | Args: 111 | url (str): The url to provide that should have the captcha image. 112 | 113 | Returns: 114 | guess (str): The guess of the user for the captcha. 115 | 116 | """ 117 | print(f'Please follow {url} and provide the solution.') 118 | try: 119 | guess = input('Captcha: ') 120 | except KeyboardInterrupt: 121 | raise CaptchaError('User interrupted.') from None 122 | return guess 123 | 124 | 125 | class Captcha2(Solver): 126 | """2captcha solver.""" 127 | 128 | def __init__(self, api_token): 129 | self.solver = self._get_client(api_token) 130 | 131 | @staticmethod 132 | def _get_client(api_token): 133 | solver = TwoCaptcha(api_token) 134 | try: 135 | balance = solver.balance() 136 | if not balance: 137 | raise InvalidOrNoBalanceApiToken('No balance left on the token.') 138 | except ApiException as msg: 139 | raise InvalidOrNoBalanceApiToken(msg) from None 140 | return solver 141 | 142 | def solve(self, url): 143 | """Presents a captcha image url and returns the captcha. 144 | 145 | Args: 146 | url (str): The url to provide that should have the captcha image. 147 | 148 | Returns: 149 | guess (str): The captcha. 150 | 151 | """ 152 | captcha_parameters = {'numeric': 4, 153 | 'minLength': 6, 154 | 'maxLength': 6, 155 | 'phrase': 0, 156 | 'caseSensitive': 1, 157 | 'calc': 0, 158 | 'lang': 'en'} 159 | try: 160 | self.logger.debug(f'Trying to get captcha image from url : {url}') 161 | response = requests.get(url, timeout=5) 162 | image = base64.b64encode(response.content).decode("utf-8") 163 | self.logger.debug('Waiting for the solved captcha from 2captcha service.') 164 | result = self.solver.normal(image, **captcha_parameters) 165 | self.logger.debug(f'Result for captcha was : {result}') 166 | except (ValidationException, NetworkException, ApiException, TimeoutException) as msg: 167 | raise CaptchaError(msg) from None 168 | return result.get('code') 169 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/awsapilib.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/awsapilib.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/awsapilib" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/awsapilib" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /awsapilib/authentication/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: utils.py 4 | # 5 | # Copyright 2020 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Main code for utils. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | import json 35 | import logging 36 | 37 | __author__ = '''Costas Tyfoxylos ''' 38 | __docformat__ = '''google''' 39 | __date__ = '''24-12-2020''' 40 | __copyright__ = '''Copyright 2020, Costas Tyfoxylos''' 41 | __credits__ = ["Costas Tyfoxylos"] 42 | __license__ = '''MIT''' 43 | __maintainer__ = '''Costas Tyfoxylos''' 44 | __email__ = '''''' 45 | __status__ = '''Development''' # "Prototype", "Development", "Production". 46 | 47 | # This is the main prefix used for logging 48 | LOGGER_BASENAME = '''utils''' 49 | LOGGER = logging.getLogger(LOGGER_BASENAME) 50 | LOGGER.addHandler(logging.NullHandler()) 51 | 52 | 53 | class HarParser: 54 | """Parses a provided har file.""" 55 | 56 | def __init__(self, har_file): 57 | self.data = self._parse(har_file) 58 | 59 | @staticmethod 60 | def _parse(har_file): 61 | try: 62 | with open(har_file, 'r', encoding='utf-8') as ifile: 63 | data = json.load(ifile) 64 | except Exception: 65 | raise ValueError(f'Could not read or parse file: {har_file}') from None 66 | return data 67 | 68 | def _get_service_calls(self, service): 69 | calls = [entry for entry in self.data['log']['entries'] 70 | if any(['aws.amazon.com/oauth' in entry['request']['url'], 71 | all([f'aws.amazon.com/{service}' in entry['request']['url'], 72 | f'aws.amazon.com/{service}/api' not in entry['request']['url']])])] 73 | return [] if not any(True for entry in calls if service in entry["request"]["url"]) else calls 74 | 75 | @staticmethod 76 | def _get_text_from_calls(calls): 77 | text = '' 78 | for entry in calls: 79 | text += f'URL : {entry["request"]["url"]}\n' 80 | text += '\tRequest Headers :\n' 81 | for header in entry['request']['headers']: 82 | text += f'\t\t{header["name"]} : {header["value"]}\n' 83 | text += "\tRequest Cookies :\n" 84 | for cookie in entry['request']['cookies']: 85 | text += f'\t\t{cookie["name"]} : {cookie["value"]}\n' 86 | text += "\tResponse Headers :\n" 87 | for header in entry['response']['headers']: 88 | text += f'\t\t{header["name"]} : {header["value"]}\n' 89 | text += "\tResponse Cookies :\n" 90 | for cookie in entry['response']['cookies']: 91 | text += f'\t\t{cookie["name"]} : {cookie["value"]}\n' 92 | return text 93 | 94 | def get_communication_for_console(self): 95 | """Returns a text of the communication of a valid login to console. 96 | 97 | Returns: 98 | text (str): Returns a text of the communication of a valid login to console. 99 | 100 | """ 101 | return self._get_text_from_calls(self._get_service_calls('console')) 102 | 103 | def get_communication_for_control_tower(self): 104 | """Returns a text of the communication of a valid login to control tower. 105 | 106 | Returns: 107 | text (str): Returns a text of the communication of a valid login to control tower. 108 | 109 | """ 110 | return self._get_text_from_calls(self._get_service_calls('controltower')) 111 | 112 | def get_communication_for_sso(self): 113 | """Returns a text of the communication of a valid login to single sign on. 114 | 115 | Returns: 116 | text (str): Returns a text of the communication of a valid login to single sign on. 117 | 118 | """ 119 | return self._get_text_from_calls(self._get_service_calls('singlesignon')) 120 | 121 | def get_communication_for_billing(self): 122 | """Returns a text of the communication of a valid login to billing. 123 | 124 | Returns: 125 | text (str): Returns a text of the communication of a valid login to billing. 126 | 127 | """ 128 | return self._get_text_from_calls(self._get_service_calls('billing')) 129 | 130 | def get_communication_for_cloudformation(self): 131 | """Returns a text of the communication of a valid login to cloud formation service. 132 | 133 | Returns: 134 | text (str): Returns a text of the communication of a valid login to cloud formation service. 135 | 136 | """ 137 | return self._get_text_from_calls(self._get_service_calls('cloudformation')) 138 | 139 | def get_communication_for_iam(self): 140 | """Returns a text of the communication of a valid login to iam service. 141 | 142 | Returns: 143 | text (str): Returns a text of the communication of a valid login to iam service. 144 | 145 | """ 146 | return self._get_text_from_calls(self._get_service_calls('iam')) 147 | 148 | def render_communication_for_console(self): 149 | """Prints a text of the communication of a valid login to console. 150 | 151 | Returns: 152 | None 153 | 154 | """ 155 | print(self.get_communication_for_console()) 156 | 157 | def render_communication_for_control_tower(self): 158 | """Prints a text of the communication of a valid login to control tower. 159 | 160 | Returns: 161 | None 162 | 163 | """ 164 | print(self.get_communication_for_control_tower()) 165 | 166 | def render_communication_for_sso(self): 167 | """Prints a text of the communication of a valid login to single sign on. 168 | 169 | Returns: 170 | None 171 | 172 | """ 173 | print(self.get_communication_for_sso()) 174 | 175 | def render_communication_for_billing(self): 176 | """Prints a text of the communication of a valid login to billing. 177 | 178 | Returns: 179 | None 180 | 181 | """ 182 | print(self.get_communication_for_billing()) 183 | 184 | def render_communication_for_cloudformation(self): 185 | """Prints a text of the communication of a valid login to cloud formation service. 186 | 187 | Returns: 188 | None 189 | 190 | """ 191 | print(self.get_communication_for_cloudformation()) 192 | 193 | def render_communication_for_iam(self): 194 | """Prints a text of the communication of a valid login iam service. 195 | 196 | Returns: 197 | None 198 | 199 | """ 200 | print(self.get_communication_for_iam()) 201 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # awsapilib documentation build configuration file, created by 5 | # sphinx-quickstart 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import sphinx_rtd_theme 19 | 20 | # If extensions (or modules to document with autodoc) are in another 21 | # directory, add these directories to sys.path here. If the directory is 22 | # relative to the documentation root, use os.path.abspath to make it 23 | # absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # Get the project root dir, which is the parent dir of this 27 | cwd = os.getcwd() 28 | project_root = os.path.dirname(cwd) 29 | 30 | # Run apidoc to traverse the project directory and add all modules to the docs 31 | import sphinx.ext.apidoc 32 | 33 | sphinx.ext.apidoc.main(argv=['-f', '-o', os.path.join(project_root, 'docs'), 34 | os.path.join(project_root, '''awsapilib''')]) 35 | 36 | # Insert the project root dir as the first element in the PYTHONPATH. 37 | # This lets us ensure that the source package is imported, and that its 38 | # version is used. 39 | sys.path.insert(0, project_root) 40 | 41 | import awsapilib 42 | 43 | # -- General configuration --------------------------------------------- 44 | 45 | # If your documentation needs a minimal Sphinx version, state it here. 46 | #needs_sphinx = '1.0' 47 | 48 | # Add any Sphinx extension module names here, as strings. They can be 49 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 50 | extensions = [ 51 | 'sphinx.ext.autodoc', 52 | 'sphinx.ext.viewcode', 53 | 'sphinx.ext.napoleon', 54 | 'sphinx.ext.todo' 55 | ] 56 | 57 | napoleon_google_docstring = True 58 | 59 | # Add any paths that contain templates here, relative to this directory. 60 | templates_path = ['_templates'] 61 | 62 | # The suffix of source filenames. 63 | source_suffix = '.rst' 64 | 65 | # The encoding of source files. 66 | #source_encoding = 'utf-8-sig' 67 | 68 | # The master toctree document. 69 | master_doc = 'index' 70 | 71 | # General information about the project. 72 | project = u'''awsapilib''' 73 | copyright = u'''2021, (Author : Costas Tyfoxylos)''' 74 | 75 | # The version info for the project you're documenting, acts as replacement 76 | # for |version| and |release|, also used in various other places throughout 77 | # the built documents. 78 | # 79 | # The short X.Y version. 80 | version = awsapilib.__version__ 81 | # The full version, including alpha/beta/rc tags. 82 | release = awsapilib.__version__ 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | #language = None 87 | 88 | # There are two options for replacing |today|: either, you set today to 89 | # some non-false value, then it is used: 90 | #today = '' 91 | # Else, today_fmt is used as the format for a strftime call. 92 | #today_fmt = '%B %d, %Y' 93 | 94 | # List of patterns, relative to source directory, that match files and 95 | # directories to ignore when looking for source files. 96 | exclude_patterns = ['_build'] 97 | 98 | # The reST default role (used for this markup: `text`) to use for all 99 | # documents. 100 | #default_role = None 101 | 102 | # If true, '()' will be appended to :func: etc. cross-reference text. 103 | #add_function_parentheses = True 104 | 105 | # If true, the current module name will be prepended to all description 106 | # unit titles (such as .. function::). 107 | #add_module_names = True 108 | 109 | # If true, sectionauthor and moduleauthor directives will be shown in the 110 | # output. They are ignored by default. 111 | #show_authors = False 112 | 113 | # The name of the Pygments (syntax highlighting) style to use. 114 | pygments_style = 'sphinx' 115 | 116 | # A list of ignored prefixes for module index sorting. 117 | #modindex_common_prefix = [] 118 | 119 | # If true, keep warnings as "system message" paragraphs in the built 120 | # documents. 121 | #keep_warnings = False 122 | 123 | 124 | # -- Options for HTML output ------------------------------------------- 125 | 126 | # The theme to use for HTML and HTML Help pages. See the documentation for 127 | # a list of builtin themes. 128 | html_theme = 'sphinx_rtd_theme' 129 | 130 | # Theme options are theme-specific and customize the look and feel of a 131 | # theme further. For a list of options available for each theme, see the 132 | # documentation. 133 | #html_theme_options = {} 134 | 135 | # Add any paths that contain custom themes here, relative to this directory. 136 | #html_theme_path = [] 137 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 138 | 139 | # The name for this set of Sphinx documents. If None, it defaults to 140 | # " v documentation". 141 | #html_title = None 142 | 143 | # A shorter title for the navigation bar. Default is the same as 144 | # html_title. 145 | #html_short_title = None 146 | 147 | # The name of an image file (relative to this directory) to place at the 148 | # top of the sidebar. 149 | #html_logo = None 150 | 151 | # The name of an image file (within the static path) to use as favicon 152 | # of the docs. This file should be a Windows icon file (.ico) being 153 | # 16x16 or 32x32 pixels large. 154 | #html_favicon = None 155 | 156 | # Add any paths that contain custom static files (such as style sheets) 157 | # here, relative to this directory. They are copied after the builtin 158 | # static files, so a file named "default.css" will overwrite the builtin 159 | # "default.css". 160 | #html_static_path = ['_static'] 161 | 162 | # If not '', a 'Last updated on:' timestamp is inserted at every page 163 | # bottom, using the given strftime format. 164 | #html_last_updated_fmt = '%b %d, %Y' 165 | 166 | # If true, SmartyPants will be used to convert quotes and dashes to 167 | # typographically correct entities. 168 | #html_use_smartypants = True 169 | 170 | # Custom sidebar templates, maps document names to template names. 171 | #html_sidebars = {} 172 | 173 | # Additional templates that should be rendered to pages, maps page names 174 | # to template names. 175 | #html_additional_pages = {} 176 | 177 | # If false, no module index is generated. 178 | #html_domain_indices = True 179 | 180 | # If false, no index is generated. 181 | #html_use_index = True 182 | 183 | # If true, the index is split into individual pages for each letter. 184 | #html_split_index = False 185 | 186 | # If true, links to the reST sources are added to the pages. 187 | #html_show_sourcelink = True 188 | 189 | # If true, "Created using Sphinx" is shown in the HTML footer. 190 | # Default is True. 191 | #html_show_sphinx = True 192 | 193 | # If true, "(C) Copyright ..." is shown in the HTML footer. 194 | # Default is True. 195 | #html_show_copyright = True 196 | 197 | # If true, an OpenSearch description file will be output, and all pages 198 | # will contain a tag referring to it. The value of this option 199 | # must be the base URL from which the finished HTML is served. 200 | #html_use_opensearch = '' 201 | 202 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 203 | #html_file_suffix = None 204 | 205 | # Output file base name for HTML help builder. 206 | htmlhelp_basename = '''awsapilibdoc''' 207 | 208 | 209 | # -- Options for LaTeX output ------------------------------------------ 210 | 211 | latex_elements = { 212 | # The paper size ('letterpaper' or 'a4paper'). 213 | #'papersize': 'letterpaper', 214 | 215 | # The font size ('10pt', '11pt' or '12pt'). 216 | #'pointsize': '10pt', 217 | 218 | # Additional stuff for the LaTeX preamble. 219 | #'preamble': '', 220 | } 221 | 222 | # Grouping the document tree into LaTeX files. List of tuples 223 | # (source start file, target name, title, author, documentclass 224 | # [howto/manual]). 225 | latex_documents = [ 226 | ('index', '''awsapilib.tex''', 227 | u'''awsapilib Documentation''', 228 | u'''Costas Tyfoxylos''', 'manual'), 229 | ] 230 | 231 | # The name of an image file (relative to this directory) to place at 232 | # the top of the title page. 233 | #latex_logo = None 234 | 235 | # For "manual" documents, if this is true, then toplevel headings 236 | # are parts, not chapters. 237 | #latex_use_parts = False 238 | 239 | # If true, show page references after internal links. 240 | #latex_show_pagerefs = False 241 | 242 | # If true, show URL addresses after external links. 243 | #latex_show_urls = False 244 | 245 | # Documents to append as an appendix to all manuals. 246 | #latex_appendices = [] 247 | 248 | # If false, no module index is generated. 249 | #latex_domain_indices = True 250 | 251 | 252 | # -- Options for manual page output ------------------------------------ 253 | 254 | # One entry per manual page. List of tuples 255 | # (source start file, name, description, authors, manual section). 256 | man_pages = [ 257 | ('index', '''awsapilib''', 258 | u'''awsapilib Documentation''', 259 | [u'''Costas Tyfoxylos'''], 1) 260 | ] 261 | 262 | # If true, show URL addresses after external links. 263 | #man_show_urls = False 264 | 265 | 266 | # -- Options for Texinfo output ---------------------------------------- 267 | 268 | # Grouping the document tree into Texinfo files. List of tuples 269 | # (source start file, target name, title, author, 270 | # dir menu entry, description, category) 271 | texinfo_documents = [ 272 | ('index', '''awsapilib''', 273 | u'''awsapilib Documentation''', 274 | u'''Costas Tyfoxylos''', 275 | '''awsapilib''', 276 | 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # If true, do not generate a @detailmenu in the "Top" node's menu. 290 | #texinfo_no_detailmenu = False 291 | -------------------------------------------------------------------------------- /awsapilib/billing/billing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: billing.py 4 | # 5 | # Copyright 2021 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Main code for billing. 28 | 29 | .. _Google Python Style Guide: 30 | http://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | import logging 35 | import time 36 | import json 37 | from datetime import datetime, timedelta 38 | 39 | from bs4 import BeautifulSoup as Bfs 40 | 41 | from awsapilib.authentication import Authenticator, LoggerMixin 42 | from awsapilib.authentication import InvalidCredentials 43 | 44 | from .billingexceptions import (InvalidCountryCode, 45 | NonEditableSetting, 46 | IAMAccessDenied, 47 | InvalidCurrency, 48 | ServerError) 49 | 50 | __author__ = '''Costas Tyfoxylos ''' 51 | __docformat__ = '''google''' 52 | __date__ = '''30-03-2021''' 53 | __copyright__ = '''Copyright 2021, Costas Tyfoxylos''' 54 | __credits__ = ["Costas Tyfoxylos"] 55 | __license__ = '''MIT''' 56 | __maintainer__ = '''Costas Tyfoxylos''' 57 | __email__ = '''''' 58 | __status__ = '''Development''' # "Prototype", "Development", "Production". 59 | 60 | # This is the main prefix used for logging 61 | LOGGER_BASENAME = '''billing''' 62 | LOGGER = logging.getLogger(LOGGER_BASENAME) 63 | LOGGER.addHandler(logging.NullHandler()) 64 | 65 | 66 | class Tax(LoggerMixin): 67 | """Models the tax settings of the billing console.""" 68 | 69 | def __init__(self, billing): 70 | self._billing = billing 71 | self._endpoint = f'{self._billing.rest_api}/taxexemption/heritage' 72 | self._endpointV2 = f'{self._billing.rest_api}/taxexemption/heritage.v2' 73 | self._available_country_codes = None 74 | 75 | @property 76 | def available_country_codes_eu(self): 77 | """The available country codes of the tax settings for eu. 78 | 79 | Returns: 80 | codes (list): Available country codes 81 | 82 | """ 83 | if self._available_country_codes is None: 84 | url = f'{self._billing.rest_api}/taxexemption/eu/vat/countries' 85 | response = self._billing.session.get(url) 86 | if response.status_code == 401: 87 | raise InvalidCredentials 88 | if not response.ok: 89 | self.logger.error(f'Failed to retrieve inheritance state, response: {response.text}') 90 | return None 91 | self._available_country_codes = response.json().get('supportedCountryCodes', []) 92 | return self._available_country_codes 93 | 94 | @property 95 | def inheritance(self): 96 | """The inheritance settings of the billing preferences. 97 | 98 | Returns: 99 | setting (bool): True if set, False otherwise. 100 | 101 | """ 102 | response = self._billing.session.get(self._endpoint) 103 | if response.status_code == 401: 104 | raise InvalidCredentials 105 | if not response.ok: 106 | self.logger.error(f'Failed to retrieve inheritance state, response: {response.text}') 107 | return None 108 | return response.json().get('customerHeritageStatus', '') == 'OptIn' 109 | 110 | @inheritance.setter 111 | def inheritance(self, value: bool): 112 | """The inheritance settings setter of the billing preferences. 113 | 114 | Returns: 115 | None 116 | 117 | """ 118 | if self.inheritance == value: 119 | return 120 | self._is_editable() 121 | payload = {'heritageStatus': 'OptIn' if value else 'OptOut'} 122 | response = self._billing.session.post(self._endpointV2, json=payload) 123 | if not response.ok: 124 | self.logger.error(f'Failed to retrieve inheritance state, response: {response.text}') 125 | 126 | def _is_editable(self): 127 | parameters = {'heritageStatus': 'OptIn'} 128 | response = self._billing.session.get(self._endpoint, params=parameters) 129 | if not response.json().get('heritageStatusEditable'): 130 | timestamp = response.json().get('effectiveTimestamp') 131 | if not timestamp: 132 | raise NonEditableSetting(f'API is not enabled: {response.json()}') 133 | unlock_time = (datetime.fromtimestamp(timestamp / 1000) + timedelta(minutes=15)) 134 | wait_time = unlock_time - datetime.now() 135 | raise NonEditableSetting(f'API is not enabled for {wait_time} more.') 136 | return True 137 | 138 | # pylint: disable=too-many-arguments 139 | def set_information(self, address, city, postal_code, legal_name, vat_number, country_code, state=None): 140 | """The inheritance settings setter of the billing preferences. 141 | 142 | Returns: 143 | None 144 | 145 | """ 146 | country_code = country_code.upper() 147 | if country_code not in self.available_country_codes_eu: 148 | raise InvalidCountryCode(f'{country_code} provided is not valid. ' 149 | f'Valid ones are {self.available_country_codes_eu}') 150 | payload = {'linkedAccounts': [{self.account_id}], 151 | 'taxRegistration': {'address': {'addressLine1': address, 152 | 'addressLine2': None, 153 | 'city': city, 154 | 'countryCode': country_code, 155 | 'postalCode': postal_code, 156 | 'state': state, 157 | }, 158 | 'authority': {'country': country_code, 159 | 'state': None}, 160 | 'legalName': legal_name, 161 | 'localTaxRegistration': False, 162 | 'registrationId': vat_number, 163 | } 164 | } 165 | 166 | files = { 167 | 'details': ('blob', json.dumps(payload), 'application/json'), 168 | } 169 | 170 | url = f'{self._billing.rest_api}/taxexemption/taxregistration.v2' 171 | response = self._billing.session.put(url, files=files) 172 | if not response.ok: 173 | self.logger.error(f'Failed to set information, response: {response.text}') 174 | return response.ok 175 | 176 | 177 | class Preferences(LoggerMixin): 178 | """Models the preferences of the billing console.""" 179 | 180 | def __init__(self, billing): 181 | self._billing = billing 182 | 183 | @property 184 | def _preferences_endpoint(self): 185 | return f'{self._billing.rest_api}/preferences' 186 | 187 | @property 188 | def pdf_invoice_by_mail(self): 189 | """The setting of the pdf invoice by email. 190 | 191 | Returns: 192 | setting (bool): True if set, False otherwise. 193 | 194 | """ 195 | endpoint = f'{self._preferences_endpoint}/invoice' 196 | response = self._billing.session.get(endpoint) 197 | if not response.ok: 198 | raise SystemError('Could not retrieve the pdf invoice by mail preferences!') 199 | return response.json().get('pdfInvoiceByEmail') == 'Y' 200 | 201 | @pdf_invoice_by_mail.setter 202 | def pdf_invoice_by_mail(self, value: bool): 203 | """The setting for the setting of the pdf invoice by email. 204 | 205 | Returns: 206 | None. 207 | 208 | """ 209 | if self.pdf_invoice_by_mail == value: 210 | return 211 | payload = {'pdfInvoiceByEmail': 'Y' if value else 'N'} 212 | endpoint = f'{self._preferences_endpoint}/invoice' 213 | response = self._billing.session.put(endpoint, json=payload) 214 | if not response.ok: 215 | self.logger.error(f'Failed to retrieve inheritance state, response: {response.text}') 216 | 217 | @property 218 | def credit_sharing(self): 219 | """The setting of the credit sharing. 220 | 221 | Returns: 222 | setting (bool): True if set, False otherwise. 223 | 224 | """ 225 | endpoint = f'{self._billing.rest_api}/sharingpreferences/getcreditsharing' 226 | response = self._billing.session.get(endpoint) 227 | if not response.ok: 228 | self.logger.error(f'Failed to retrieve credit sharing state, response: {response.text}') 229 | return {} 230 | return response.json().get('creditEnabled') 231 | 232 | @credit_sharing.setter 233 | def credit_sharing(self, value: bool): 234 | """The setter of the setting of the credit sharing. 235 | 236 | Returns: 237 | None. 238 | 239 | """ 240 | if self.credit_sharing == value: 241 | return 242 | endpoint = f'{self._billing.rest_api}/sharingpreferences/setcreditsharing' 243 | payload = {'creditEnabled': bool(value), 'creditShareAccountExceptions': []} 244 | response = self._billing.session.put(endpoint, json=payload) 245 | if not response.ok: 246 | self.logger.error(f'Failed to retrieve credit sharing state, response: {response.text}') 247 | 248 | 249 | class Billing(LoggerMixin): 250 | """Models Control Tower by wrapping around service catalog.""" 251 | 252 | def __init__(self, arn, region=None): 253 | self.aws_authenticator = Authenticator(arn) 254 | self.session = self._get_authenticated_session() 255 | self.region = region or self.aws_authenticator.region 256 | self.rest_api = 'https://us-east-1.console.aws.amazon.com/billing/rest/v1.0' 257 | self._sor_info_ = None 258 | self._payment_instrument_ids = None 259 | self._marketplace_id = None 260 | 261 | def _get_authenticated_session(self): 262 | return self.aws_authenticator.get_billing_authenticated_session() 263 | 264 | @property 265 | def account_id(self): 266 | """Account id.""" 267 | return self._sor_info.get('accountId') 268 | 269 | @property 270 | def sor_id(self): 271 | """Sor id.""" 272 | return self._sor_info.get('sor', {}).get('sorId') 273 | 274 | @property 275 | def _sor_info(self): 276 | if self._sor_info_ is None: 277 | url = f'{self.rest_api}/sellerofrecord/getsorbyaccount' 278 | response = self.session.get(url) 279 | if response.status_code == 401: 280 | raise InvalidCredentials 281 | if not response.ok: 282 | self.logger.error(f'Could not retrieve sor info, response: {response.text}') 283 | self._sor_info_ = [] 284 | self._sor_info_ = response.json() 285 | return self._sor_info_ 286 | 287 | @property 288 | def tax(self): 289 | """Tax settings. 290 | 291 | Returns: 292 | tax (Tax): The tax settings object. 293 | 294 | """ 295 | return Tax(self) 296 | 297 | @property 298 | def preferences(self): 299 | """Preferences settings. 300 | 301 | Returns: 302 | preferences (Preferences): The preferences settings object. 303 | 304 | """ 305 | return Preferences(self) 306 | 307 | @property 308 | def currency(self): 309 | """Currency settings. 310 | 311 | Returns: 312 | currency (str): The currency set. 313 | 314 | """ 315 | url = f'{self.rest_api}/account/fxpaymentinfopapyrus' 316 | response = self.session.get(url) 317 | if not response.ok: 318 | self.logger.error(f'Failed to retrieve currency setting, response: {response.text}') 319 | return None 320 | return response.json().get('currencyPreference') 321 | 322 | @currency.setter 323 | def currency(self, value): 324 | """Setter for currency settings. 325 | 326 | Returns: 327 | None 328 | 329 | """ 330 | url = f'{self.rest_api}/account/currencypreference' 331 | response = self.session.put(url, json=value.upper()) 332 | if not response.ok: 333 | if response.json().get('type') == 'InvalidParameterException': 334 | raise InvalidCurrency(value) 335 | self.logger.error(f'Failed to set currency setting, response: {response.text}') 336 | 337 | def _validate_iam_access(self): 338 | url = f'{self.rest_api}/account/iamaccess' 339 | response = self.session.get(url) 340 | if not response.ok: 341 | self.logger.error(f'Failed to get iam access settings, response: {response.text}') 342 | return {} 343 | if response.json().get('type') == 'AccessDeniedException': 344 | raise IAMAccessDenied 345 | return response.json() 346 | 347 | @property 348 | def iam_access(self): 349 | """IAM access to billing setting.""" 350 | return self._validate_iam_access().get('billingConsoleAccessEnabled', False) 351 | 352 | @iam_access.setter 353 | def iam_access(self, value): 354 | """IAM access to billing setting.""" 355 | data = self._validate_iam_access() 356 | if data: 357 | url = f'{self.rest_api}/account/iamaccess' 358 | data.update({'billingConsoleAccessEnabled': bool(value)}) 359 | response = self.session.put(url, json=data) 360 | if not response.ok: 361 | self.logger.error(f'No IAM role access provided to the console, response: {response.text}') 362 | 363 | @property 364 | def _region_states(self): 365 | url = f'{self.rest_api}/account/accountregionstates' 366 | response = self.session.get(url) 367 | if response.status_code == 401: 368 | raise InvalidCredentials 369 | if not response.ok: 370 | self.logger.error(f'Could not retrieve region states, response: {response.text}') 371 | return [] 372 | return response.json().get('accountRegionStateList', []) 373 | 374 | @property 375 | def enabled_region_states(self): 376 | """Enabled region states.""" 377 | return [region.get('regionName') for region in self._region_states 378 | if region.get('regionState') == 'ENABLED'] 379 | 380 | @property 381 | def disabled_region_states(self): 382 | """Disabled region states.""" 383 | return [region.get('regionName') for region in self._region_states 384 | if region.get('regionState') == 'DISABLED'] 385 | 386 | @property 387 | def payment_cards(self): 388 | """Payment cards.""" 389 | if self._payment_instrument_ids is None: 390 | url = 'https://console.aws.amazon.com/billing/rest/ppg-proxy' 391 | headers = {'x-requested-with': 'XMLHttpRequest', 392 | 'Operation': 'AWSPaymentPreferenceGateway.Get'} 393 | payload = {'content': {'Input': {'arn': f'arn:aws:payments:us-east-1:{self.account_id}:' 394 | f'paymentpreference:PaymentInstrument'}, 395 | 'Operation': 'com.amazon.aws.payments.gateway.coral.' 396 | 'paymentpreference.operations#Get', 397 | 'Service': 'com.amazon.aws.payments.gateway.coral.paymentpreference.' 398 | 'service#AWSPaymentPreferenceGateway'}, 399 | 'headers': {'Content-Type': 'application/json', 400 | 'X-Amz-Date': time.strftime("%a, %d %b %Y %I:%M:%S %Z", time.gmtime()), 401 | 'X-Amz-Target': 'AWSPaymentPreferenceGateway.Get'}, 402 | 'region': 'us-east-1'} 403 | response = self.session.post(url, headers=headers, json=payload) 404 | if response.status_code == 401: 405 | raise InvalidCredentials 406 | if not response.ok: 407 | self.logger.error(f'Could not retrieve payment instrument id, response: {response.text}') 408 | raise ServerError 409 | metadata = response.json().get('Output', {}).get('paymentPreferenceWithMetadata', {}) 410 | self._payment_instrument_ids = [PaymentCard(self, data) 411 | for data in metadata.get('value', {}).get('chargeInstruments', [])] 412 | return self._payment_instrument_ids 413 | 414 | @property 415 | def market_place_id(self): 416 | """Marker place id of account.""" 417 | if self._marketplace_id is None: 418 | url = 'https://console.aws.amazon.com/billing/home?' 419 | response = self.session.get(url) 420 | if response.status_code == 401: 421 | raise InvalidCredentials 422 | if not response.ok: 423 | self.logger.error(f'Could not retrieve market place id, response: {response.text}') 424 | raise ServerError 425 | soup = Bfs(response.text, features="html.parser") 426 | self._marketplace_id = soup.find('input', {'id': 'marketPlace'}).attrs.get('value') 427 | return self._marketplace_id 428 | 429 | # def get_attribute(self, path): 430 | # response = self.session.get(f'{self.rest_api}/{path}') 431 | # return response 432 | 433 | 434 | class PaymentCard(LoggerMixin): 435 | """Models a payment card.""" 436 | 437 | def __init__(self, billing, data): 438 | self._billing = billing 439 | self._arn = data.get('arn') 440 | self._data_ = None 441 | 442 | @property 443 | def _data(self): 444 | if self._data_ is None: 445 | url = f'{self._billing.rest_api}/billingcontactaddress/get' 446 | parameters = {'marketplaceId': self._billing.market_place_id, 447 | 'piArn': self._arn} 448 | response = self._billing.session.get(url, params=parameters) 449 | if response.status_code == 401: 450 | raise InvalidCredentials 451 | if not response.ok: 452 | self.logger.error(f'Could not retrieve market place id, response: {response.text}') 453 | raise ServerError 454 | self._data_ = response.json() 455 | return self._data_ 456 | 457 | @property 458 | def _address(self): 459 | return self._data.get('address', {}) 460 | 461 | @property 462 | def address_id(self): 463 | """Address id.""" 464 | return self._address.get('addressId') 465 | 466 | @property 467 | def address_line_1(self): 468 | """First line of the address settings.""" 469 | return self._address.get('addressLine1') 470 | 471 | @property 472 | def address_line_2(self): 473 | """Second line of the address settings.""" 474 | return self._address.get('addressLine2') 475 | 476 | @property 477 | def city(self): 478 | """City.""" 479 | return self._address.get('city') 480 | 481 | @property 482 | def company(self): 483 | """Company.""" 484 | return self._address.get('company') 485 | 486 | @property 487 | def country_code(self): 488 | """Country code.""" 489 | return self._address.get('countryCode') 490 | 491 | @property 492 | def email_address_list(self): 493 | """Email address list.""" 494 | return self._address.get('emailAddressList', []) 495 | 496 | @property 497 | def full_name(self): 498 | """Full name.""" 499 | return self._address.get('fullName') 500 | 501 | @property 502 | def phone_number(self): 503 | """Phone number.""" 504 | return self._address.get('phoneNumber') 505 | 506 | @property 507 | def postal_code(self): 508 | """Postal code.""" 509 | return self._address.get('postalCode') 510 | 511 | @property 512 | def state(self): 513 | """State.""" 514 | return self._address.get('state') 515 | 516 | @property 517 | def payment_instrument_arn(self): 518 | """Payment instrument arn.""" 519 | return self._data.get('paymentInstrumentArn') 520 | -------------------------------------------------------------------------------- /awsapilib/sso/entities/entities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: entities.py 4 | # 5 | # Copyright 2020 Sayantan Khanra, Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | """ 26 | Main code for entities. 27 | 28 | .. _Google Python Style Guide: 29 | http://google.github.io/styleguide/pyguide.html 30 | 31 | """ 32 | 33 | import logging 34 | import json 35 | 36 | from awsapilib.authentication import LoggerMixin 37 | 38 | __author__ = '''Sayantan Khanra , Costas Tyfoxylos ''' 39 | __docformat__ = '''google''' 40 | __date__ = '''18-05-2020''' 41 | __copyright__ = '''Copyright 2020, Sayantan Khanra, Costas Tyfoxylos''' 42 | __credits__ = ["Sayantan Khanra", "Costas Tyfoxylos"] 43 | __license__ = '''MIT''' 44 | __maintainer__ = '''Sayantan Khanra, Costas Tyfoxylos''' 45 | __email__ = ''', ''' 46 | __status__ = '''Development''' # "Prototype", "Development", "Production". 47 | 48 | # This is the main prefix used for logging 49 | LOGGER_BASENAME = '''entities''' 50 | LOGGER = logging.getLogger(LOGGER_BASENAME) 51 | LOGGER.addHandler(logging.NullHandler()) 52 | 53 | 54 | class Entity(LoggerMixin): 55 | """The core entity.""" 56 | 57 | def __init__(self, sso_instance, data): 58 | self._sso = sso_instance 59 | self._data = self._parse_data(data) 60 | 61 | def _parse_data(self, data): 62 | if not isinstance(data, dict): 63 | self.logger.error(f'Invalid data received :{data}') 64 | data = {} 65 | return data 66 | 67 | 68 | class Group(Entity): 69 | """Models the group object of AWS SSO.""" 70 | 71 | def __init__(self, sso_instance, data): 72 | super().__init__(sso_instance, data) 73 | self.url = f'{sso_instance.api_url}/userpool' 74 | 75 | @property 76 | def id(self): # pylint: disable=invalid-name 77 | """The id of the group. 78 | 79 | Returns: 80 | id (str) : The id of the group 81 | 82 | """ 83 | return self._data.get('GroupId') 84 | 85 | @property 86 | def name(self): 87 | """The name of the group. 88 | 89 | Returns: 90 | name (str) : The name of the group 91 | 92 | """ 93 | return self._data.get('GroupName', '') 94 | 95 | @property 96 | def description(self): 97 | """The description of the group. 98 | 99 | Returns: 100 | description (str) : The description of the group 101 | 102 | """ 103 | return self._data.get('Description', '') 104 | 105 | @property 106 | def users(self): 107 | """The users in the group. 108 | 109 | Returns: 110 | users (list): The users part of the group 111 | 112 | """ 113 | content_payload = {'GroupId': self.id, 114 | 'MaxResults': 100} 115 | target = 'com.amazonaws.swbup.service.SWBUPService.ListMembersInGroup' 116 | for user in self._sso._get_paginated_results(content_payload=content_payload, # pylint: disable=protected-access 117 | path='userpool', 118 | target='ListMembersInGroup', 119 | amz_target=target, 120 | object_group='Members', 121 | url=self.url): 122 | yield self._sso.get_user_by_id(user.get('Id')) 123 | 124 | 125 | class Account(Entity): 126 | """Models the Account object of AWS SSO.""" 127 | 128 | @property 129 | def url(self): 130 | """Url for the account. 131 | 132 | Returns: 133 | url (str): The url of the account 134 | 135 | """ 136 | return self._sso.endpoint_url 137 | 138 | @property 139 | def name(self): 140 | """The name of the application. 141 | 142 | Returns: 143 | name (str): The name of the application 144 | 145 | """ 146 | return self._data.get('Name') 147 | 148 | @property 149 | def email(self): 150 | """The name of the application. 151 | 152 | Returns: 153 | email (str) : The name of the application 154 | 155 | """ 156 | return self._data.get('Email') 157 | 158 | @property 159 | def id(self): # pylint: disable=invalid-name 160 | """The id of the application. 161 | 162 | Returns: 163 | id (str): The id of the application 164 | 165 | """ 166 | return self._data.get('Id') 167 | 168 | @property 169 | def arn(self): 170 | """The arn of the application. 171 | 172 | Returns: 173 | arn (str): The arn of the application 174 | 175 | """ 176 | return self._data.get('Arn') 177 | 178 | @property 179 | def status(self): 180 | """The status of the application. 181 | 182 | Returns: 183 | status (str): The status of the application 184 | 185 | """ 186 | return self._data.get('Status') 187 | 188 | def provision_saml_provider(self): 189 | """Creates the SAMl provider. 190 | 191 | Returns: 192 | arn (str): The arn of the SAMl provider 193 | 194 | """ 195 | target = 'com.amazon.switchboard.service.SWBService.ProvisionSAMLProvider' 196 | payload = self._sso.get_api_payload(content_string={'applicationInstanceId': self.instance_id 197 | }, 198 | target='ProvisionSAMLProvider', 199 | path='/control/', 200 | x_amz_target=target) 201 | self.logger.debug('Trying to create saml provider for aws account with payload: %s', payload) 202 | response = self._sso.session.post(self.url, json=payload) 203 | if not response.ok: 204 | self.logger.error(response.text) 205 | return {} 206 | return response.json() 207 | 208 | @property 209 | def instance_id(self): 210 | """The instance id of the Account. 211 | 212 | Returns: 213 | instance_id (str): The instance id of the account 214 | 215 | """ 216 | instance_id = self._retrieve_instance_id() 217 | if not instance_id: 218 | instance_id = self._provision_application_instance_for_aws_account() 219 | return instance_id 220 | 221 | def _provision_application_instance_for_aws_account(self): 222 | target = 'com.amazon.switchboard.service.SWBService.ProvisionApplicationInstanceForAWSAccount' 223 | payload = self._sso.get_api_payload(content_string={'accountId': self.id, 224 | 'accountEmail': self.email, 225 | 'accountName': self.name 226 | }, 227 | target='ProvisionApplicationInstanceForAWSAccount', 228 | path='/control/', 229 | x_amz_target=target) 230 | self.logger.debug('Trying to get instance id for aws account with payload: %s', payload) 231 | response = self._sso.session.post(self.url, json=payload) 232 | if not response.ok: 233 | self.logger.error(response.text) 234 | return None 235 | return response.json().get('applicationInstance', {}).get('instanceId', None) 236 | 237 | def _retrieve_instance_id(self): 238 | account_id = self.id 239 | target = 'com.amazon.switchboard.service.SWBService.GetApplicationInstanceForAWSAccount' 240 | payload = self._sso.get_api_payload(content_string={'awsAccountId': account_id}, 241 | target='GetApplicationInstanceForAWSAccount', 242 | path='/control/', 243 | x_amz_target=target) 244 | self.logger.debug('Trying to get instance id for aws account with payload: %s', payload) 245 | response = self._sso.session.post(self.url, json=payload) 246 | if not response.ok: 247 | self.logger.error(response.text) 248 | return None 249 | return response.json().get('applicationInstance', {}).get('instanceId', None) 250 | 251 | @property 252 | def associated_profiles(self): 253 | """The associated profiles with the Account. 254 | 255 | Returns: 256 | associated_profiles (list): The profiles associated with the Account 257 | 258 | """ 259 | target = 'com.amazon.switchboard.service.SWBService.ListAWSAccountProfiles' 260 | payload = self._sso.get_api_payload(content_string={'instanceId': self.instance_id}, 261 | target='ListAWSAccountProfiles', 262 | path='/control/', 263 | x_amz_target=target) 264 | self.logger.debug('Trying to provision application profile for aws account with payload: %s', payload) 265 | response = self._sso.session.post(self.url, json=payload) 266 | if not response.ok: 267 | self.logger.error(response.text) 268 | return [] 269 | return response.json().get('profileList', []) 270 | 271 | 272 | class User(Entity): 273 | """Models the user object of SSO.""" 274 | 275 | @property 276 | def url(self): 277 | """Url for the user. 278 | 279 | Returns: 280 | url (str): The url for the user 281 | 282 | """ 283 | return f'{self._sso.api_url}/userpool' 284 | 285 | @property 286 | def status(self): 287 | """The status of the user. 288 | 289 | Returns: 290 | status (str): The status of the user 291 | 292 | """ 293 | return self._data.get('Active') 294 | 295 | @property 296 | def created_at(self): 297 | """The date and time of the users's activation. 298 | 299 | Returns: 300 | created_at (datetime): The datetime object of when the user was activated 301 | 302 | """ 303 | return self._data.get('Meta', {}).get('CreatedAt') 304 | 305 | @property 306 | def updated_at(self): 307 | """The date and time of the users's status change. 308 | 309 | Returns: 310 | updated_at (datetime): The datetime object of when the user had last changed status 311 | 312 | """ 313 | return self._data.get('Meta', {}).get('UpdatedAt') 314 | 315 | @property 316 | def emails(self): 317 | """The date and time of the users's last password change. 318 | 319 | Returns: 320 | emails (datetime): The datetime object of when the user last changed password 321 | 322 | """ 323 | return self._data.get('UserAttributes').get('emails', {}).get('ComplexListValue', '') 324 | 325 | @property 326 | def _name(self): 327 | return self._data.get('UserAttributes').get('name', {}).get('ComplexValue', {}) 328 | 329 | @property 330 | def first_name(self): 331 | """The first name of the user. 332 | 333 | Returns: 334 | first_name (str): The first name of the user 335 | 336 | """ 337 | return self._name.get('givenName', {}).get('StringValue', '') 338 | 339 | @property 340 | def last_name(self): 341 | """The last name of the user. 342 | 343 | Returns: 344 | last_name (str): The last name of the user 345 | 346 | """ 347 | return self._name.get('familyName', {}).get('StringValue', '') 348 | 349 | @property 350 | def id(self): # pylint: disable=invalid-name 351 | """The manager of the user. 352 | 353 | Returns: 354 | id (str): The manager of the user 355 | 356 | """ 357 | return self._data.get('UserId') 358 | 359 | @property 360 | def name(self): 361 | """The manager of the user. 362 | 363 | Returns: 364 | name (str): The manager of the user 365 | 366 | """ 367 | return self._data.get('UserName') 368 | 369 | @property 370 | def display_name(self): 371 | """The display name of the user. 372 | 373 | Returns: 374 | display_name (str): The display name of the user 375 | 376 | """ 377 | return self._data.get('UserAttributes', {}).get('displayName', {}).get('StringValue') 378 | 379 | @property 380 | def groups(self): 381 | """The groups associated with the user. 382 | 383 | Returns: 384 | groups (list): The groups associated with the user 385 | 386 | """ 387 | content_payload = {'UserId': self.id, 388 | 'MaxResults': 100} 389 | target = 'com.amazonaws.swbup.service.SWBUPService.ListGroupsForUser' 390 | for group in self._sso._get_paginated_results(content_payload=content_payload, # pylint: disable=protected-access 391 | path='userpool', 392 | target='ListGroupsForUser', 393 | amz_target=target, 394 | object_group='Groups', 395 | url=self.url): 396 | yield self._sso.get_group_by_id(group.get('GroupId')) 397 | 398 | 399 | class PermissionSet(Entity): 400 | """Models the permission set object of SSO.""" 401 | 402 | @property 403 | def url(self): 404 | """Url of the permission set. 405 | 406 | Returns: 407 | url (str): The url of the permission set 408 | 409 | """ 410 | return self._sso.endpoint_url 411 | 412 | @property 413 | def description(self): 414 | """The description of the permission set. 415 | 416 | Returns: 417 | description (str): The description of the permission set 418 | 419 | """ 420 | return self._data.get('Description') 421 | 422 | @property 423 | def id(self): # pylint: disable=invalid-name 424 | """The id of the permission set. 425 | 426 | Returns: 427 | id (str): The id of the permission set 428 | 429 | """ 430 | return self._data.get('Id') 431 | 432 | @property 433 | def name(self): 434 | """The name of the permission set. 435 | 436 | Returns: 437 | name (str): The name of the permission set 438 | 439 | """ 440 | return self._data.get('Name') 441 | 442 | @property 443 | def ttl(self): 444 | """The ttl of the permission set. 445 | 446 | Returns: 447 | ttl (str): The ttl of the permission set 448 | 449 | """ 450 | return self._data.get('ttl') 451 | 452 | @property 453 | def creation_date(self): 454 | """The creation date of the permission set. 455 | 456 | Returns: 457 | creation_date (str): The creation date of the permission set 458 | 459 | """ 460 | return self._data.get('CreationDate') 461 | 462 | @property 463 | def relay_state(self): 464 | """The relay_state of the permission_set. 465 | 466 | Returns: 467 | relay_state (str): The relayState of the permission_set 468 | 469 | """ 470 | return self._data.get('relayState') 471 | 472 | @property 473 | def permission_policy(self): 474 | """The permission policy of the permission_set. 475 | 476 | Returns: 477 | permission_policy (dict): The permission policy of the permission_set 478 | 479 | """ 480 | target = 'com.amazon.switchboard.service.SWBService.GetPermissionsPolicy' 481 | content_string = {'permissionSetId': self.id} 482 | payload = self._sso.get_api_payload(content_string=content_string, 483 | target='GetPermissionsPolicy', 484 | path='/control/', 485 | x_amz_target=target) 486 | self.logger.debug('Getting permission policy for permission_set with payload of %s:', payload) 487 | response = self._sso.session.post(self.url, json=payload) 488 | if not response.ok: 489 | self.logger.error(response.text) 490 | return None 491 | return response.json() 492 | 493 | @property 494 | def provisioned_accounts(self): 495 | """The provisioned accounts with the permission set. 496 | 497 | Returns: 498 | list: Accounts provisioned with the permission set 499 | 500 | """ 501 | content_payload = {'permissionSetId': self.id, 502 | 'onlyOutOfSync': 'false'} 503 | target = 'com.amazon.switchboard.service.SWBService.ListAccountsWithProvisionedPermissionSet' 504 | for account_id in self._sso._get_paginated_results(content_payload=content_payload, # pylint: disable=protected-access 505 | path='control', 506 | target='ListAccountsWithProvisionedPermissionSet', 507 | amz_target=target, 508 | object_group='accountIds', 509 | next_token_marker='marker', 510 | url=self._sso.endpoint_url): 511 | yield self._sso.get_account_by_id(account_id) 512 | 513 | def assign_custom_policy_to_permission_set(self, policy_document): 514 | """Assign Custom policy to a permission_set. 515 | 516 | Args: 517 | permission_set_name: The name of the permission_set . 518 | policy_document: The policy for the permission_set 519 | Returns: 520 | Bool: True or False 521 | 522 | """ 523 | content_string = {'permissionSetId': self.id, 524 | 'policyDocument': json.dumps(policy_document)} 525 | target = 'com.amazon.switchboard.service.SWBService.PutPermissionsPolicy' 526 | payload = self._sso.get_api_payload(content_string=content_string, 527 | target='PutPermissionsPolicy', 528 | path='/control/', 529 | x_amz_target=target) 530 | self.logger.debug('Assigning custom policy to permission set with payload %s:', payload) 531 | response = self._sso.session.post(self.url, json=payload) 532 | if not response.ok: 533 | self.logger.error(response.text) 534 | else: 535 | if self.provisioned_accounts: # pylint: disable=using-constant-test 536 | for account in self.provisioned_accounts: 537 | self.logger.debug('Updating associated account %s', account.name) 538 | self._sso._provision_application_profile_for_aws_account_instance(self.name, account.name) # pylint: disable=protected-access 539 | return response.ok 540 | 541 | def delete_custom_policy_from_permission_set(self): 542 | """Assign Custom policy to a permission_set. 543 | 544 | Returns: 545 | Bool: True or False 546 | 547 | """ 548 | content_string = {'permissionSetId': self.id} 549 | target = 'com.amazon.switchboard.service.SWBService.DeletePermissionsPolicy' 550 | payload = self._sso.get_api_payload(content_string=content_string, 551 | target='DeletePermissionsPolicy', 552 | path='/control/', 553 | x_amz_target=target) 554 | response = self._sso.session.post(self.url, json=payload) 555 | if not response.ok: 556 | self.logger.error(response.text) 557 | else: 558 | if self.provisioned_accounts: # pylint: disable=using-constant-test 559 | for account in self.provisioned_accounts: 560 | self.logger.debug('Updating associated account %s', account.name) 561 | self._sso._provision_application_profile_for_aws_account_instance(self.name, account.name) # pylint: disable=protected-access 562 | return response.ok 563 | 564 | def update(self, description=' ', relay_state='', ttl=''): 565 | """The relayState of the permission_set. 566 | 567 | Args: 568 | description: Description for the permission set 569 | relay_state: The relay state for the permission set. 570 | https://docs.aws.amazon.com/singlesignon/latest/userguide/howtopermrelaystate.html 571 | ttl: session duration 572 | 573 | Returns: 574 | bool: True or False 575 | 576 | """ 577 | content_string = {'permissionSetId': self.id, 578 | 'description': description if description != ' ' else self.description, 579 | 'ttl': ttl if ttl else self.ttl, 580 | 'relayState': relay_state if relay_state else self.relay_state} 581 | target = 'com.amazon.switchboard.service.SWBService.UpdatePermissionSet' 582 | payload = self._sso.get_api_payload(content_string=content_string, 583 | target='UpdatePermissionSet', 584 | path='/control/', 585 | x_amz_target=target) 586 | self.logger.debug('Posting to url %s payload of %s:', self.url, payload) 587 | response = self._sso.session.post(self.url, json=payload) 588 | if not response.ok: 589 | self.logger.error(response.text) 590 | return response.ok 591 | --------------------------------------------------------------------------------