├── .editorconfig ├── .gitignore ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── aws_role_credentials ├── __init__.py ├── actions.py ├── cli.py ├── metadata.py └── models.py ├── docs ├── Makefile ├── authors.rst ├── conf.py ├── contributing.rst ├── history.rst ├── index.rst ├── installation.rst ├── make.bat ├── readme.rst └── usage.rst ├── pavement.py ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── helper.py ├── test_acceptance.py ├── test_actions.py ├── test_cli.py └── test_models.py └── tox.ini /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | htmlcov 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Complexity 39 | output/*.html 40 | output/*/index.html 41 | 42 | # Sphinx 43 | docs/_build 44 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Peter Gillard-Moss 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /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 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/petergillardmoss/aws_role_credentials/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | AWS Role Credentials could always use more documentation, whether as part of the 40 | official AWS Role Credentials docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/petergillardmoss/aws_role_credentials/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `aws_role_credentials` for local development. 59 | 60 | 1. Fork the `aws_role_credentials` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone git@github.com:your_name_here/aws_role_credentials.git 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv aws_role_credentials 68 | $ cd aws_role_credentials/ 69 | $ pip install -r requirements_dev.txt 70 | $ python setup.py develop 71 | 72 | 4. Create a branch for local development:: 73 | 74 | $ git checkout -b name-of-your-bugfix-or-feature 75 | 76 | Now you can make your changes locally. 77 | 78 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 79 | 80 | $ flake8 aws_role_credentials tests 81 | $ python setup.py test 82 | $ tox 83 | 84 | To get flake8 and tox, just pip install them into your virtualenv. 85 | 86 | 6. Commit your changes and push your branch to GitHub:: 87 | 88 | $ git add . 89 | $ git commit -m "Your detailed description of your changes." 90 | $ git push origin name-of-your-bugfix-or-feature 91 | 92 | 7. Submit a pull request through the GitHub website. 93 | 94 | Pull Request Guidelines 95 | ----------------------- 96 | 97 | Before you submit a pull request, check that it meets these guidelines: 98 | 99 | 1. The pull request should include tests. 100 | 2. If the pull request adds functionality, the docs should be updated. Put 101 | your new functionality into a function with a docstring, and add the 102 | feature to the list in README.rst. 103 | 3. The pull request should work for Python 2.6, 2.7, 3.3, and 3.4, and for PyPy. Check 104 | https://snap-ci.com/github_repositories/ThoughtWorksInc/aws_role_credentials/pulls 105 | and make sure that the tests pass for all supported Python versions. 106 | 107 | Tips 108 | ---- 109 | 110 | To run a subset of tests:: 111 | 112 | $ python -m unittest tests.test_aws_role_credentials 113 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.1.0 (2015-01-11) 7 | --------------------- 8 | 9 | * First release on PyPI. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Peter Gillard-Moss 2 | All rights reserved. 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs clean 2 | define BROWSER_PYSCRIPT 3 | import os, webbrowser, sys 4 | try: 5 | from urllib import pathname2url 6 | except: 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 13 | 14 | help: 15 | @echo "clean - remove all build, test, coverage and Python artifacts" 16 | @echo "clean-build - remove build artifacts" 17 | @echo "clean-pyc - remove Python file artifacts" 18 | @echo "clean-test - remove test and coverage artifacts" 19 | @echo "lint - check style with flake8" 20 | @echo "test - run tests quickly with the default Python" 21 | @echo "test-all - run tests on every Python version with tox" 22 | @echo "coverage - check code coverage quickly with the default Python" 23 | @echo "docs - generate Sphinx HTML documentation, including API docs" 24 | @echo "release - package and upload a release" 25 | @echo "dist - package" 26 | @echo "install - install the package to the active Python's site-packages" 27 | 28 | clean: clean-build clean-pyc clean-test 29 | 30 | clean-build: 31 | rm -fr build/ 32 | rm -fr dist/ 33 | rm -fr .eggs/ 34 | find . -name '*.egg-info' -exec rm -fr {} + 35 | find . -name '*.egg' -exec rm -fr {} + 36 | 37 | clean-pyc: 38 | find . -name '*.pyc' -exec rm -f {} + 39 | find . -name '*.pyo' -exec rm -f {} + 40 | find . -name '*~' -exec rm -f {} + 41 | find . -name '__pycache__' -exec rm -fr {} + 42 | 43 | clean-test: 44 | rm -fr .tox/ 45 | rm -f .coverage 46 | rm -fr htmlcov/ 47 | 48 | lint: 49 | flake8 aws_role_credentials tests 50 | 51 | test: 52 | python setup.py test 53 | 54 | test-all: 55 | tox 56 | 57 | coverage: 58 | coverage run --source aws_role_credentials setup.py test 59 | coverage report -m 60 | coverage html 61 | $(BROWSER) htmlcov/index.html 62 | 63 | docs: 64 | rm -f docs/aws_role_credentials.rst 65 | rm -f docs/modules.rst 66 | sphinx-apidoc -o docs/ aws_role_credentials 67 | $(MAKE) -C docs clean 68 | $(MAKE) -C docs html 69 | $(BROWSER) docs/_build/html/index.html 70 | 71 | servedocs: docs 72 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 73 | 74 | release: clean 75 | python setup.py sdist upload 76 | python setup.py bdist_wheel upload 77 | 78 | dist: clean 79 | python setup.py sdist 80 | python setup.py bdist_wheel 81 | ls -l dist 82 | 83 | install: clean 84 | python setup.py install 85 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | AWS Role Credentials 3 | =============================== 4 | 5 | .. image:: https://img.shields.io/pypi/v/aws_role_credentials.svg 6 | :target: https://pypi.python.org/pypi/aws_role_credentials 7 | 8 | .. image:: https://snap-ci.com/ThoughtWorksInc/aws_role_credentials/branch/master/build_image 9 | :target: https://snap-ci.com/ThoughtWorksInc/aws_role_credentials/branch/master 10 | 11 | Generates AWS credentials for roles using STS and writes them to ```~/.aws/credentials``` 12 | 13 | Usage 14 | ===== 15 | 16 | Simply pipe a SAML assertion into awssaml 17 | 18 | .. code-block:: shell 19 | 20 | # create credentials from saml assertion 21 | $ oktaauth -u jobloggs | aws_role_credentials saml --profile dev 22 | 23 | Or for assuming a known role name: 24 | 25 | .. code-block:: shell 26 | 27 | # create credentials from saml assertion using a known role ARN 28 | $ oktaauth -u jobloggs | aws_role_credentials saml --profile dev --role-arn arn:aws:iam::098765432109:role/ReadOnly 29 | 30 | Or for assuming a role using an IAM user: 31 | 32 | .. code-block:: shell 33 | 34 | # create credentials from an iam user 35 | $ aws_role_credentials user \ 36 | arn:aws:iam::111111:role/dev jobloggs-session \ 37 | --profile dev 38 | 39 | For roles that require MFA: 40 | 41 | .. code-block:: shell 42 | 43 | # create credentials from an iam user with mfa 44 | $ aws_role_credentials user \ 45 | arn:aws:iam::111111:role/dev jobloggs-session \ 46 | --profile dev \ 47 | --mfa-serial-number arn:aws:iam::111111:mfa/Jo \ 48 | --mfa-token 102345 49 | 50 | Transient mode 51 | -------------- 52 | 53 | ```aws_role_credentials``` also supports 'transient' mode where the 54 | credentials are passed to a command as environment variables within 55 | the process. This adds an extra layer of safety and convinience. 56 | 57 | To use transient mode simply pass a command to the ```--exec``` option 58 | like so: 59 | 60 | .. code-block:: shell 61 | 62 | # run 'aws s3 ls' with the generated role credentials from an iam user 63 | $ aws_role_credentials user \ 64 | arn:aws:iam::111111:role/dev jobloggs-session \ 65 | --exec 'aws s3 ls' 66 | 67 | 68 | Options 69 | ======= 70 | 71 | --profile Use a specific profile in your credential file (e.g. Development). Defaults to sts. 72 | --region The region to use. Overrides config/env settings. Defaults to us-east-1. 73 | --role-arn Optional `role ARN`_ to use when multiple roles are available. 74 | --exec The command to execute with the AWS credentials 75 | 76 | .. _role ARN: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html 77 | 78 | Thanks 79 | ====== 80 | 81 | Thanks to Quint Van Deman of AWS for demonstrating how to do this. https://blogs.aws.amazon.com/security/post/Tx1LDN0UBGJJ26Q/How-to-Implement-Federated-API-and-CLI-Access-Using-SAML-2-0-and-AD-FS 82 | 83 | 84 | Authors 85 | ======= 86 | 87 | * Peter Gillard-Moss 88 | -------------------------------------------------------------------------------- /aws_role_credentials/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = 'Peter Gillard-Moss' 4 | __email__ = 'pgillard@thoughtworks.com' 5 | __version__ = '0.6.4' 6 | -------------------------------------------------------------------------------- /aws_role_credentials/actions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import boto.sts 4 | import os 5 | import shlex 6 | from getpass import getpass 7 | from subprocess import Popen 8 | 9 | from aws_role_credentials.models import SamlAssertion, AwsCredentialsFile 10 | 11 | 12 | class Actions: 13 | 14 | def __init__(self, 15 | credentials_filename, 16 | profile, 17 | region, 18 | quiet, 19 | **kwargs): 20 | 21 | self.credentials_filename = credentials_filename 22 | self.profile = profile 23 | self.region = region 24 | self.quiet = quiet 25 | 26 | @staticmethod 27 | def print_credentials(credentials_filename, profile, credentials): 28 | print('\n\n----------------------------------------------------------------') 29 | print('Your credentials have been stored in the AWS configuration file {0} under the {1} profile.'.format(credentials_filename, profile)) 30 | print('Note that they will expire at {0}.'.format(credentials.expiration)) 31 | print('You may safely rerun this script at any time to refresh your credentials.') 32 | print('To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile {0} ec2 describe-instances).'.format(profile)) 33 | print('----------------------------------------------------------------\n\n') 34 | 35 | @staticmethod 36 | def persist_credentials(credentials_filename, 37 | profile, region, token, quiet, **kwargs): 38 | AwsCredentialsFile(credentials_filename).add_profile(profile, 39 | region, 40 | token.credentials) 41 | if not quiet: 42 | Actions.print_credentials(credentials_filename, 43 | profile, 44 | token.credentials) 45 | 46 | @staticmethod 47 | def credentials_handler(credentials_filename, 48 | profile, 49 | region, quiet, **kwargs): 50 | 51 | return lambda token: Actions.persist_credentials(credentials_filename, 52 | profile, 53 | region, 54 | token, 55 | quiet) 56 | 57 | @staticmethod 58 | def exec_with_credentials(region, command, token): 59 | env = os.environ.copy() 60 | 61 | env["AWS_ACCESS_KEY_ID"] = token.credentials.access_key 62 | env["AWS_SECRET_ACCESS_KEY"] = token.credentials.secret_key 63 | env["AWS_SESSION_TOKEN"] = token.credentials.session_token 64 | env["AWS_DEFAULT_REGION"] = region 65 | 66 | Popen(shlex.split(command), 67 | env=env, 68 | shell=False).wait() 69 | 70 | @staticmethod 71 | def exec_handler(region, exec_command, **kwargs): 72 | return lambda token: Actions.exec_with_credentials(region, exec_command, token) 73 | 74 | @staticmethod 75 | def saml_token(region, assertion, **kwargs): 76 | assertion = SamlAssertion(assertion) 77 | roles = assertion.roles() 78 | if kwargs.get('role_arn', False): 79 | for i, role in enumerate(roles): 80 | if role['role'] == kwargs['role_arn']: 81 | role = roles[i] 82 | break 83 | elif len(roles) > 1: 84 | print('Please select the role you would like to assume:') 85 | for i, role in enumerate(roles): 86 | print('[{}] - {}'.format(i, role['role'])) 87 | while True: 88 | # We use getpass() instead of input() here because we are already listening for stdin as part of 89 | # read_stdin() 90 | selectedroleindex = getpass('Selection: ') 91 | try: 92 | role = roles[int(selectedroleindex)] 93 | break 94 | except (IndexError, ValueError): 95 | print('Invalid selection, please try again...') 96 | else: 97 | role = roles[0] 98 | 99 | conn = boto.sts.connect_to_region(region, anon=True) 100 | return conn.assume_role_with_saml(role['role'], role['principle'], 101 | assertion.encode()) 102 | 103 | @staticmethod 104 | def user_token(region, role_arn, session_name, 105 | mfa_serial_number=None, mfa_token=None, 106 | **kwargs): 107 | conn = boto.sts.connect_to_region(region) 108 | 109 | return conn.assume_role(role_arn, session_name, 110 | mfa_serial_number=mfa_serial_number, 111 | mfa_token=mfa_token) 112 | -------------------------------------------------------------------------------- /aws_role_credentials/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | import argparse 8 | import logging 9 | import os 10 | 11 | from os.path import expanduser 12 | from aws_role_credentials import metadata 13 | from aws_role_credentials.actions import Actions 14 | 15 | log = logging.getLogger('aws_role_credentials') 16 | 17 | 18 | def configurelogging(): 19 | log.setLevel(logging.DEBUG) 20 | stderrlog = logging.StreamHandler() 21 | stderrlog.setFormatter(logging.Formatter("%(message)s")) 22 | log.addHandler(stderrlog) 23 | 24 | 25 | def read_stdin(): 26 | try: 27 | return ''.join([line for line in sys.stdin]) 28 | except KeyboardInterrupt: 29 | sys.stdout.flush() 30 | pass 31 | 32 | 33 | def token_action(args): 34 | if args['exec_command']: 35 | return Actions.exec_handler(**args) 36 | return Actions.credentials_handler(**args) 37 | 38 | 39 | def saml_action(args): 40 | args['assertion'] = read_stdin() 41 | 42 | token_action(args)(Actions.saml_token(**args)) 43 | 44 | 45 | def user_action(args): 46 | token_action(args)(Actions.user_token(**args)) 47 | 48 | 49 | def create_parser(prog, epilog, 50 | saml_action=saml_action, 51 | user_action=user_action): 52 | arg_parser = argparse.ArgumentParser( 53 | prog=prog, 54 | formatter_class=argparse.RawDescriptionHelpFormatter, 55 | description=metadata.description, 56 | epilog=epilog) 57 | subparsers = arg_parser.add_subparsers() 58 | 59 | parent_parser = argparse.ArgumentParser(add_help=False) 60 | 61 | parent_parser.add_argument( 62 | '-V', '--version', 63 | action='version', 64 | version='{0} {1}'.format(metadata.project, metadata.version)) 65 | 66 | parent_parser.add_argument( 67 | '--profile', type=str, 68 | default='sts', 69 | help='Use a specific profile in your credential file.') 70 | 71 | parent_parser.add_argument( 72 | '--region', type=str, 73 | default='us-east-1', 74 | help='The region to use. Overrides config/env settings.') 75 | 76 | parent_parser.add_argument( 77 | '--role-arn', type=str, 78 | help='Optional role ARN to use when multiple roles are available.') 79 | 80 | parent_parser.add_argument( 81 | '--exec', type=str, 82 | dest='exec_command', 83 | help='If present then the string is read as a command to execute with the AWS credentials set as environment variables.') 84 | 85 | parent_parser.add_argument( 86 | '-q', '--quiet', 87 | action='store_true', 88 | help='Do not print helpful info including token expiration on successful authentication.') 89 | 90 | saml_parser = subparsers.add_parser('saml', 91 | description='Assume role using SAML assertion', 92 | parents=[parent_parser]) 93 | 94 | saml_parser.set_defaults(func=saml_action) 95 | 96 | user_parser = subparsers.add_parser('user', 97 | description='Assume role using IAM user', 98 | parents=[parent_parser]) 99 | 100 | user_parser.add_argument( 101 | 'role_arn', type=str, 102 | help='The arn of the role to assume', 103 | ) 104 | 105 | user_parser.add_argument( 106 | 'session_name', type=str, 107 | help='An identifier for the assumed role session.') 108 | 109 | user_parser.add_argument( 110 | '--mfa-serial-number', type=str, 111 | help='An identifier of the MFA device that is associated with the user.') 112 | 113 | user_parser.add_argument( 114 | '--mfa-token', type=str, 115 | help='The value provided by the MFA device.') 116 | 117 | user_parser.set_defaults(func=user_action) 118 | 119 | return arg_parser 120 | 121 | 122 | def main(argv): 123 | configurelogging() 124 | 125 | """Program entry point. 126 | 127 | :param argv: command-line arguments 128 | :type argv: :class:`list` 129 | """ 130 | author_strings = [] 131 | for name, email in zip(metadata.authors, metadata.emails): 132 | author_strings.append('Author: {0} <{1}>'.format(name, email)) 133 | 134 | epilog = ''' 135 | {project} {version} 136 | 137 | {authors} 138 | URL: <{url}> 139 | '''.format( 140 | project=metadata.project, 141 | version=metadata.version, 142 | authors='\n'.join(author_strings), 143 | url=metadata.url) 144 | 145 | arg_parser = create_parser(argv[0], epilog) 146 | config = arg_parser.parse_args(args=argv[1:]) 147 | 148 | log.info(epilog) 149 | 150 | credentials_dir = expanduser('~/.aws') 151 | 152 | if not os.path.exists(credentials_dir): 153 | os.makedirs(credentials_dir) 154 | 155 | config.credentials_filename = os.path.join(credentials_dir, 'credentials') 156 | 157 | config.func(vars(config)) 158 | 159 | return 0 160 | 161 | 162 | def entry_point(): 163 | """Zero-argument entry point for use with setuptools/distribute.""" 164 | raise SystemExit(main(sys.argv)) 165 | 166 | 167 | if __name__ == '__main__': 168 | entry_point() 169 | -------------------------------------------------------------------------------- /aws_role_credentials/metadata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Project metadata 3 | 4 | Information describing the project. 5 | """ 6 | 7 | # The package name, which is also the "UNIX name" for the project. 8 | package = 'aws_role_credentials' 9 | project = "AWS role credentials" 10 | project_no_spaces = project.replace(' ', '') 11 | version = '0.6.4' 12 | description = "Generates AWS credentials for roles using STS" 13 | authors = ['Peter Gillard-Moss'] 14 | authors_string = ', '.join(authors) 15 | emails = ['pgillard@thoughtworks.com'] 16 | license = 'Apache 2.0' 17 | copyright = '2015 Thoughtworks Inc' 18 | url = 'https://github.com/petergillardmoss/aws_role_credentials' 19 | -------------------------------------------------------------------------------- /aws_role_credentials/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import base64 4 | import configparser 5 | import xml.etree.ElementTree as ET 6 | 7 | 8 | class SamlAssertion: 9 | 10 | def __init__(self, assertion): 11 | self.assertion = assertion 12 | 13 | @staticmethod 14 | def split_roles(roles): 15 | return [(y.strip()) 16 | for y 17 | in roles.text.split(',')] 18 | 19 | @staticmethod 20 | def sort_roles(roles): 21 | return sorted(roles, 22 | key=lambda role: 'saml-provider' in role) 23 | 24 | def roles(self): 25 | attributes = ET.fromstring(self.assertion).getiterator('{urn:oasis:names:tc:SAML:2.0:assertion}Attribute') 26 | 27 | roles_attributes = [x for x 28 | in attributes 29 | if x.get('Name') == 'https://aws.amazon.com/SAML/Attributes/Role'] 30 | 31 | roles_values = [(x.getiterator('{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue')) 32 | for x 33 | in roles_attributes] 34 | 35 | return [(dict(zip(['role', 'principle'], 36 | self.sort_roles(self.split_roles(x))))) 37 | for x 38 | in roles_values[0]] 39 | 40 | def encode(self): 41 | return base64.b64encode(self.assertion.encode('utf8')) 42 | 43 | 44 | class AwsCredentialsFile: 45 | 46 | def __init__(self, filename): 47 | self.filename = filename 48 | return 49 | 50 | def _add_profile(self, name, profile): 51 | 52 | config = configparser.ConfigParser(interpolation=None) 53 | try: 54 | config.read_file(open(self.filename, 'r')) 55 | except: 56 | pass 57 | 58 | if not config.has_section(name): 59 | config.add_section(name) 60 | 61 | [(config.set(name, k, v)) 62 | for k, v in profile.items()] 63 | 64 | with open(self.filename, 'w+') as configfile: 65 | config.write(configfile) 66 | 67 | def add_profile(self, name, region, credentials): 68 | name = str(name) 69 | self._add_profile( 70 | name, { 71 | u'output': u'json', 72 | u'region': str(region), 73 | u'aws_access_key_id': str(credentials.access_key), 74 | u'aws_secret_access_key': str(credentials.secret_key), 75 | u'aws_security_token': str(credentials.session_token), 76 | u'aws_session_token': str(credentials.session_token) 77 | }) 78 | -------------------------------------------------------------------------------- /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/aws_role_credentials.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aws_role_credentials.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/aws_role_credentials" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aws_role_credentials" 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 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aws_role_credentials documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jul 9 22:26:36 2013. 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 | 19 | # If extensions (or modules to document with autodoc) are in another 20 | # directory, add these directories to sys.path here. If the directory is 21 | # relative to the documentation root, use os.path.abspath to make it 22 | # absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # Get the project root dir, which is the parent dir of this 26 | cwd = os.getcwd() 27 | project_root = os.path.dirname(cwd) 28 | 29 | # Insert the project root dir as the first element in the PYTHONPATH. 30 | # This lets us ensure that the source package is imported, and that its 31 | # version is used. 32 | sys.path.insert(0, project_root) 33 | 34 | import aws_role_credentials 35 | 36 | # -- General configuration --------------------------------------------- 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | #needs_sphinx = '1.0' 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 43 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix of source filenames. 49 | source_suffix = '.rst' 50 | 51 | # The encoding of source files. 52 | #source_encoding = 'utf-8-sig' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # General information about the project. 58 | project = u'AWS Role Credentials' 59 | copyright = u'2015, Peter Gillard-Moss' 60 | 61 | # The version info for the project you're documenting, acts as replacement 62 | # for |version| and |release|, also used in various other places throughout 63 | # the built documents. 64 | # 65 | # The short X.Y version. 66 | version = aws_role_credentials.__version__ 67 | # The full version, including alpha/beta/rc tags. 68 | release = aws_role_credentials.__version__ 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | #language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to 75 | # some non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = ['_build'] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built 106 | # documents. 107 | #keep_warnings = False 108 | 109 | 110 | # -- Options for HTML output ------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'default' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a 117 | # theme further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | #html_theme_path = [] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as 129 | # html_title. 130 | #html_short_title = None 131 | 132 | # The name of an image file (relative to this directory) to place at the 133 | # top of the sidebar. 134 | #html_logo = None 135 | 136 | # The name of an image file (within the static path) to use as favicon 137 | # of the docs. This file should be a Windows icon file (.ico) being 138 | # 16x16 or 32x32 pixels large. 139 | #html_favicon = None 140 | 141 | # Add any paths that contain custom static files (such as style sheets) 142 | # here, relative to this directory. They are copied after the builtin 143 | # static files, so a file named "default.css" will overwrite the builtin 144 | # "default.css". 145 | html_static_path = ['_static'] 146 | 147 | # If not '', a 'Last updated on:' timestamp is inserted at every page 148 | # bottom, using the given strftime format. 149 | #html_last_updated_fmt = '%b %d, %Y' 150 | 151 | # If true, SmartyPants will be used to convert quotes and dashes to 152 | # typographically correct entities. 153 | #html_use_smartypants = True 154 | 155 | # Custom sidebar templates, maps document names to template names. 156 | #html_sidebars = {} 157 | 158 | # Additional templates that should be rendered to pages, maps page names 159 | # to template names. 160 | #html_additional_pages = {} 161 | 162 | # If false, no module index is generated. 163 | #html_domain_indices = True 164 | 165 | # If false, no index is generated. 166 | #html_use_index = True 167 | 168 | # If true, the index is split into individual pages for each letter. 169 | #html_split_index = False 170 | 171 | # If true, links to the reST sources are added to the pages. 172 | #html_show_sourcelink = True 173 | 174 | # If true, "Created using Sphinx" is shown in the HTML footer. 175 | # Default is True. 176 | #html_show_sphinx = True 177 | 178 | # If true, "(C) Copyright ..." is shown in the HTML footer. 179 | # Default is True. 180 | #html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages 183 | # will contain a tag referring to it. The value of this option 184 | # must be the base URL from which the finished HTML is served. 185 | #html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | #html_file_suffix = None 189 | 190 | # Output file base name for HTML help builder. 191 | htmlhelp_basename = 'aws_role_credentialsdoc' 192 | 193 | 194 | # -- Options for LaTeX output ------------------------------------------ 195 | 196 | latex_elements = { 197 | # The paper size ('letterpaper' or 'a4paper'). 198 | #'papersize': 'letterpaper', 199 | 200 | # The font size ('10pt', '11pt' or '12pt'). 201 | #'pointsize': '10pt', 202 | 203 | # Additional stuff for the LaTeX preamble. 204 | #'preamble': '', 205 | } 206 | 207 | # Grouping the document tree into LaTeX files. List of tuples 208 | # (source start file, target name, title, author, documentclass 209 | # [howto/manual]). 210 | latex_documents = [ 211 | ('index', 'aws_role_credentials.tex', 212 | u'AWS Role Credentials Documentation', 213 | u'Peter Gillard-Moss', 'manual'), 214 | ] 215 | 216 | # The name of an image file (relative to this directory) to place at 217 | # the top of the title page. 218 | #latex_logo = None 219 | 220 | # For "manual" documents, if this is true, then toplevel headings 221 | # are parts, not chapters. 222 | #latex_use_parts = False 223 | 224 | # If true, show page references after internal links. 225 | #latex_show_pagerefs = False 226 | 227 | # If true, show URL addresses after external links. 228 | #latex_show_urls = False 229 | 230 | # Documents to append as an appendix to all manuals. 231 | #latex_appendices = [] 232 | 233 | # If false, no module index is generated. 234 | #latex_domain_indices = True 235 | 236 | 237 | # -- Options for manual page output ------------------------------------ 238 | 239 | # One entry per manual page. List of tuples 240 | # (source start file, name, description, authors, manual section). 241 | man_pages = [ 242 | ('index', 'aws_role_credentials', 243 | u'AWS Role Credentials Documentation', 244 | [u'Peter Gillard-Moss'], 1) 245 | ] 246 | 247 | # If true, show URL addresses after external links. 248 | #man_show_urls = False 249 | 250 | 251 | # -- Options for Texinfo output ---------------------------------------- 252 | 253 | # Grouping the document tree into Texinfo files. List of tuples 254 | # (source start file, target name, title, author, 255 | # dir menu entry, description, category) 256 | texinfo_documents = [ 257 | ('index', 'aws_role_credentials', 258 | u'AWS Role Credentials Documentation', 259 | u'Peter Gillard-Moss', 260 | 'aws_role_credentials', 261 | 'One line description of project.', 262 | 'Miscellaneous'), 263 | ] 264 | 265 | # Documents to append as an appendix to all manuals. 266 | #texinfo_appendices = [] 267 | 268 | # If false, no module index is generated. 269 | #texinfo_domain_indices = True 270 | 271 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 272 | #texinfo_show_urls = 'footnote' 273 | 274 | # If true, do not generate a @detailmenu in the "Top" node's menu. 275 | #texinfo_no_detailmenu = False 276 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. aws_role_credentials documentation master file, created by 2 | sphinx-quickstart on Tue Jul 9 22:26:36 2013. 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 AWS Role Credentials's documentation! 7 | ====================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | readme 15 | installation 16 | usage 17 | contributing 18 | authors 19 | history 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | $ easy_install aws_role_credentials 8 | 9 | Or, if you have virtualenvwrapper installed:: 10 | 11 | $ mkvirtualenv aws_role_credentials 12 | $ pip install aws_role_credentials 13 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aws_role_credentials.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aws_role_credentials.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Usage 3 | ======== 4 | 5 | To use AWS Role Credentials in a project:: 6 | 7 | import aws_role_credentials 8 | -------------------------------------------------------------------------------- /pavement.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | sys.path.append('.') 5 | 6 | CODE_DIRECTORY='aws_role_credentials' 7 | 8 | from paver.easy import task, consume_args 9 | 10 | @task 11 | @consume_args 12 | def run(args): 13 | """Run the package's main script. All arguments are passed to it.""" 14 | # The main script expects to get the called executable's name as 15 | # argv[0]. However, paver doesn't provide that in args. Even if it did (or 16 | # we dove into sys.argv), it wouldn't be useful because it would be paver's 17 | # executable. So we just pass the package name in as the executable name, 18 | # since it's close enough. This should never be seen by an end user 19 | # installing through Setuptools anyway. 20 | from aws_role_credentials.cli import main 21 | raise SystemExit(main([CODE_DIRECTORY] + args)) 22 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | bumpversion==0.5.3 2 | wheel==0.23.0 3 | watchdog==0.8.3 4 | flake8==2.4.1 5 | mock==1.3.0 6 | tox==2.1.1 7 | configparser==3.5.0b2 8 | coverage==4.0 9 | Sphinx==1.3.1 10 | pyfakefs==2.7 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.6.4 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:aws_role_credentials/metadata.py] 7 | 8 | [bumpversion:file:aws_role_credentials/__init__.py] 9 | 10 | [wheel] 11 | universal = 1 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | import os 11 | import sys 12 | import imp 13 | 14 | CODE_DIRECTORY = 'aws_role_credentials' 15 | 16 | metadata = imp.load_source( 17 | 'metadata', os.path.join(CODE_DIRECTORY, 'metadata.py')) 18 | 19 | with open('README.rst') as readme_file: 20 | readme = readme_file.read() 21 | 22 | with open('HISTORY.rst') as history_file: 23 | history = history_file.read().replace('.. :changelog:', '') 24 | 25 | requirements = [ 26 | 'boto', 27 | 'six', 28 | ] 29 | 30 | if sys.version_info < (3, 0): 31 | requirements.append('configparser') 32 | 33 | # as of Python >= 2.7 and >= 3.2, the argparse module is maintained within 34 | # the Python standard library, otherwise we install it as a separate package 35 | if sys.version_info < (2, 7) or (3, 0) <= sys.version_info < (3, 3): 36 | requirements.append('argparse') 37 | 38 | 39 | test_requirements = [ 40 | 'mock', 41 | 'pyfakefs', 42 | 'unittest2' 43 | ] 44 | 45 | setup( 46 | name=metadata.package, 47 | version=metadata.version, 48 | author=metadata.authors[0], 49 | author_email=metadata.emails[0], 50 | url=metadata.url, 51 | description=metadata.description, 52 | long_description=readme + '\n\n' + history, 53 | packages=[ 54 | 'aws_role_credentials', 55 | ], 56 | package_dir={'aws_role_credentials': 57 | 'aws_role_credentials'}, 58 | include_package_data=True, 59 | install_requires=requirements, 60 | license="ISCL", 61 | zip_safe=False, 62 | keywords='aws_role_credentials', 63 | classifiers=[ 64 | 'Development Status :: 2 - Pre-Alpha', 65 | 'Intended Audience :: Developers', 66 | 'License :: OSI Approved :: ISC License (ISCL)', 67 | 'Natural Language :: English', 68 | 'Programming Language :: Python :: 2', 69 | 'Programming Language :: Python :: 2.6', 70 | 'Programming Language :: Python :: 2.7', 71 | 'Programming Language :: Python :: 3', 72 | 'Programming Language :: Python :: 3.3', 73 | 'Programming Language :: Python :: 3.4', 74 | ], 75 | test_suite='tests', 76 | tests_require=test_requirements, 77 | entry_points={ 78 | 'console_scripts': [ 79 | 'aws_role_credentials = aws_role_credentials.cli:entry_point' 80 | ] 81 | } 82 | ) 83 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | def saml_assertion(roles): 2 | attribute_value = ''' 3 | {0} 4 | ''' 5 | 6 | roles_values = [(attribute_value.format(x)) for x in roles] 7 | 8 | return ''' 9 | 10 | 11 | 12 | 13 | {0} 14 | 15 | 16 | 17 | '''.format("".join(roles_values)) 18 | 19 | 20 | def read_config_file(filename): 21 | with open(filename, "r") as testfile: 22 | config = [(l.replace('\n', '')) 23 | for l in testfile.readlines()] 24 | 25 | return config 26 | 27 | 28 | def write_config_file(filename, *lines): 29 | with open(filename, 'w') as testfile: 30 | for line in lines: 31 | testfile.write("%s\n" % line) 32 | 33 | 34 | class Struct: 35 | def __init__(self, entries): 36 | self.__dict__.update(**entries) 37 | -------------------------------------------------------------------------------- /tests/test_acceptance.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import mock 4 | 5 | if sys.version_info < (2, 7): 6 | import unittest2 as unittest 7 | else: 8 | import unittest 9 | 10 | from pyfakefs import fake_filesystem_unittest 11 | import six 12 | 13 | from os.path import expanduser 14 | from mock import MagicMock 15 | from tests.helper import saml_assertion, read_config_file, Struct 16 | from aws_role_credentials import cli 17 | from six.moves import StringIO 18 | 19 | 20 | class TestAcceptance(fake_filesystem_unittest.TestCase): 21 | HOME = expanduser('~/') 22 | TEST_FILE = os.path.join(HOME, '.aws/credentials') 23 | 24 | def setUp(self): 25 | self.patcher = mock.patch('aws_role_credentials.cli.configurelogging') 26 | self.patcher.start() 27 | 28 | self.setUpPyfakefs() 29 | if not os.path.exists(self.HOME): 30 | os.makedirs(self.HOME) 31 | 32 | def tearDown(self): 33 | self.patcher.stop() 34 | pass 35 | 36 | @mock.patch('aws_role_credentials.actions.boto.sts') 37 | def test_credentials_are_generated_from_saml(self, mock_sts): 38 | mock_conn = MagicMock() 39 | mock_conn.assume_role_with_saml.return_value = Struct({'credentials': 40 | Struct({'expiration': 'SAML_TOKEN_EXPIRATION', 41 | 'access_key': 'SAML_ACCESS_KEY', 42 | 'secret_key': 'SAML_SECRET_KEY', 43 | 'session_token': 'SAML_TOKEN'})}) 44 | mock_sts.connect_to_region.return_value = mock_conn 45 | 46 | sys.stdin = StringIO(saml_assertion(['arn:aws:iam::1111:role/DevRole,arn:aws:iam::1111:saml-provider/IDP'])) 47 | cli.main(['test.py', 'saml', 48 | '--profile', 'test-profile', 49 | '--region', 'un-south-1']) 50 | 51 | six.assertCountEqual(self, 52 | read_config_file(self.TEST_FILE), 53 | ['[test-profile]', 54 | 'output = json', 55 | 'region = un-south-1', 56 | 'aws_access_key_id = SAML_ACCESS_KEY', 57 | 'aws_secret_access_key = SAML_SECRET_KEY', 58 | 'aws_security_token = SAML_TOKEN', 59 | 'aws_session_token = SAML_TOKEN', 60 | '']) 61 | 62 | @mock.patch('aws_role_credentials.actions.boto.sts') 63 | def test_credentials_are_generated_from_user(self, mock_sts): 64 | mock_conn = MagicMock() 65 | mock_conn.assume_role.return_value = Struct({'credentials': 66 | Struct({'expiration': 'SAML_TOKEN_EXPIRATION', 67 | 'access_key': 'SAML_ACCESS_KEY', 68 | 'secret_key': 'SAML_SECRET_KEY', 69 | 'session_token': 'SAML_TOKEN'})}) 70 | mock_sts.connect_to_region.return_value = mock_conn 71 | 72 | arn = 'arn:role/developer' 73 | session_name = 'dev-session' 74 | 75 | cli.main(['test.py', 'user', arn, session_name, 76 | '--profile', 'test-profile', 77 | '--region', 'un-south-1']) 78 | 79 | six.assertCountEqual(self, read_config_file(self.TEST_FILE), 80 | ['[test-profile]', 81 | 'output = json', 82 | 'region = un-south-1', 83 | 'aws_access_key_id = SAML_ACCESS_KEY', 84 | 'aws_secret_access_key = SAML_SECRET_KEY', 85 | 'aws_security_token = SAML_TOKEN', 86 | 'aws_session_token = SAML_TOKEN', 87 | '']) 88 | 89 | @mock.patch('aws_role_credentials.actions.Popen') 90 | @mock.patch('aws_role_credentials.actions.boto.sts') 91 | def test_credentials_exec_command(self, mock_sts, mock_popen): 92 | mock_conn = MagicMock() 93 | mock_conn.assume_role.return_value = Struct({'credentials': 94 | Struct({'expiration': 'SAML_TOKEN_EXPIRATION', 95 | 'access_key': 'SAML_ACCESS_KEY', 96 | 'secret_key': 'SAML_SECRET_KEY', 97 | 'session_token': 'SAML_TOKEN'})}) 98 | 99 | cli.main(['test.py', 'user', 'arn:role/developer', 100 | 'dev-session', 101 | '--exec', 'echo hello']) 102 | 103 | args, kwargs = mock_popen.call_args 104 | 105 | self.assertTrue(['echo', 'hello'] in args) 106 | -------------------------------------------------------------------------------- /tests/test_actions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import mock 4 | from mock import MagicMock 5 | 6 | if sys.version_info < (2, 7): 7 | import unittest2 as unittest 8 | else: 9 | import unittest 10 | 11 | from pyfakefs import fake_filesystem_unittest 12 | import six 13 | 14 | from tests.helper import saml_assertion, read_config_file, Struct 15 | from aws_role_credentials.actions import Actions 16 | 17 | 18 | class TestActions(unittest.TestCase): 19 | @mock.patch('aws_role_credentials.actions.boto.sts') 20 | def test_credentials_are_generated_from_saml(self, mock_sts): 21 | stub_token = Struct({'credentials': None}) 22 | mock_conn = MagicMock() 23 | mock_conn.assume_role_with_saml.return_value = stub_token 24 | mock_sts.connect_to_region.return_value = mock_conn 25 | 26 | assertion = saml_assertion(['arn:aws:iam::1111:role/DevRole,arn:aws:iam::1111:saml-provider/IDP']) 27 | 28 | token = Actions.saml_token('un-south-1', assertion) 29 | 30 | self.assertEqual(token, stub_token) 31 | 32 | @mock.patch('aws_role_credentials.actions.boto.sts') 33 | def test_credentials_are_generated_from_user(self, mock_sts): 34 | stub_token = Struct({'credentials': None}) 35 | 36 | mock_conn = MagicMock() 37 | mock_conn.assume_role.return_value = stub_token 38 | mock_sts.connect_to_region.return_value = mock_conn 39 | 40 | arn = 'arn:role/developer' 41 | session_name = 'dev-session' 42 | 43 | token = Actions.user_token('un-south-1', 44 | arn, session_name) 45 | 46 | mock_conn.assume_role.assert_called_with(arn, session_name, 47 | mfa_serial_number=None, 48 | mfa_token=None) 49 | 50 | self.assertEqual(token, stub_token) 51 | 52 | @mock.patch('aws_role_credentials.actions.boto.sts') 53 | def test_mfa_is_passed_to_sts(self, mock_sts): 54 | stub_token = Struct({'credentials': None}) 55 | 56 | mock_conn = MagicMock() 57 | mock_conn.assume_role.return_value = stub_token 58 | mock_sts.connect_to_region.return_value = mock_conn 59 | 60 | arn = 'arn:role/developer' 61 | session_name = 'dev-session' 62 | 63 | Actions.user_token('un-south-1', 64 | arn, session_name, 65 | mfa_serial_number='arn:11111', 66 | mfa_token='123456') 67 | 68 | mock_conn.assume_role.assert_called_with(arn, session_name, 69 | mfa_serial_number='arn:11111', 70 | mfa_token='123456') 71 | 72 | @mock.patch('aws_role_credentials.actions.Popen') 73 | def test_exec_setups_environment_variables(self, mock_popen): 74 | token = Struct({'credentials': 75 | Struct({'access_key': 'TEST_ACCESS_KEY', 76 | 'secret_key': 'TEST_SECRET_KEY', 77 | 'session_token': 'TEST_TOKEN', 78 | 'expiration': 'TEST_EXPIRATION'})}) 79 | 80 | with mock.patch('os.environ') as mock_env: 81 | mock_env.copy.return_value = {} 82 | 83 | Actions.exec_with_credentials('un-south-1', 84 | 'echo hello', token) 85 | 86 | mock_popen.assert_called_with(['echo', 'hello'], 87 | env={'AWS_ACCESS_KEY_ID': 'TEST_ACCESS_KEY', 88 | 'AWS_DEFAULT_REGION': 'un-south-1', 89 | 'AWS_SECRET_ACCESS_KEY': 'TEST_SECRET_KEY', 90 | 'AWS_SESSION_TOKEN': 'TEST_TOKEN'}, 91 | shell=False) 92 | 93 | 94 | class TestConfigActions(fake_filesystem_unittest.TestCase): 95 | TEST_FILE = "/test/file" 96 | 97 | def setUp(self): 98 | self.setUpPyfakefs() 99 | os.mkdir('/test') 100 | 101 | def tearDown(self): 102 | pass 103 | 104 | def test_credentials_are_generated_from_token(self): 105 | token = Struct({'credentials': 106 | Struct({'access_key': 'SAML_ACCESS_KEY', 107 | 'secret_key': 'SAML_SECRET_KEY', 108 | 'session_token': 'SAML_TOKEN', 109 | 'expiration': 'TEST_EXPIRATION'})}) 110 | 111 | Actions.persist_credentials(self.TEST_FILE, 112 | 'test-profile', 113 | 'un-south-1', token, True) 114 | 115 | six.assertCountEqual(self, read_config_file(self.TEST_FILE), 116 | ['[test-profile]', 117 | 'output = json', 118 | 'region = un-south-1', 119 | 'aws_access_key_id = SAML_ACCESS_KEY', 120 | 'aws_secret_access_key = SAML_SECRET_KEY', 121 | 'aws_security_token = SAML_TOKEN', 122 | 'aws_session_token = SAML_TOKEN', 123 | '']) 124 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info < (2, 7): 3 | import unittest2 as unittest 4 | else: 5 | import unittest 6 | from mock import Mock 7 | 8 | from aws_role_credentials.cli import create_parser 9 | 10 | 11 | class TestArgParsing(unittest.TestCase): 12 | def setUp(self): 13 | self.saml_action = Mock() 14 | self.user_action = Mock() 15 | self.parser = create_parser('test', None, 16 | self.saml_action, 17 | self.user_action) 18 | 19 | def test_profile_arg(self): 20 | parsed = self.parser.parse_args(['saml', '--profile', 'test']) 21 | self.assertEqual(parsed.profile, 'test') 22 | 23 | def test_profile_default(self): 24 | parsed = self.parser.parse_args(['saml']) 25 | self.assertEqual(parsed.profile, 'sts') 26 | 27 | def test_region_arg(self): 28 | parsed = self.parser.parse_args(['saml', '--region', 'un-test-1']) 29 | self.assertEqual(parsed.region, 'un-test-1') 30 | 31 | def test_region_default(self): 32 | parsed = self.parser.parse_args(['saml']) 33 | self.assertEqual(parsed.region, 'us-east-1') 34 | 35 | def test_exec_arg(self): 36 | parsed = self.parser.parse_args(['saml', '--exec', 'echo this']) 37 | self.assertEquals(parsed.exec_command, 'echo this') 38 | 39 | def test_saml_subcommand(self): 40 | parsed = self.parser.parse_args(['saml']) 41 | 42 | parsed.func() 43 | 44 | self.saml_action.assert_called_with() 45 | 46 | def test_user_subcommand(self): 47 | parsed = self.parser.parse_args(['user', 'test-arn', 'test-session']) 48 | 49 | self.assertEqual(parsed.role_arn, 'test-arn') 50 | self.assertEqual(parsed.session_name, 'test-session') 51 | 52 | parsed.func() 53 | 54 | self.user_action.assert_called_with() 55 | 56 | def test_user_subcommand_requires_positional_args(self): 57 | with self.assertRaises(SystemExit): 58 | self.parser.parse_args(['user']) 59 | 60 | def test_user_subcommand_requires_session_name(self): 61 | with self.assertRaises(SystemExit): 62 | self.parser.parse_args(['user', 'role-arn']) 63 | 64 | def test_user_subcommand_with_mfa(self): 65 | parsed = self.parser.parse_args(['user', 'test-arn', 'test-session', 66 | '--mfa-serial-number', 'test-mfa-serial', 67 | '--mfa-token', 'test-mfa-token']) 68 | 69 | self.assertEqual(parsed.role_arn, 'test-arn') 70 | self.assertEqual(parsed.session_name, 'test-session') 71 | self.assertEqual(parsed.mfa_serial_number, 'test-mfa-serial') 72 | self.assertEqual(parsed.mfa_token, 'test-mfa-token') 73 | 74 | parsed.func() 75 | 76 | self.user_action.assert_called_with() 77 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_aws_role_credentials 6 | ---------------------------------- 7 | 8 | Tests for `aws_role_credentials` module. 9 | """ 10 | import os 11 | import sys 12 | if sys.version_info < (2, 7): 13 | import unittest2 as unittest 14 | else: 15 | import unittest 16 | 17 | from pyfakefs import fake_filesystem_unittest 18 | import six 19 | 20 | from tests.helper import saml_assertion, write_config_file, read_config_file, Struct 21 | from aws_role_credentials.models import SamlAssertion, AwsCredentialsFile 22 | 23 | 24 | class TestSamlAssertion(unittest.TestCase): 25 | def test_roles_are_extracted(self): 26 | assertion = saml_assertion(['arn:aws:iam::1111:role/DevRole,arn:aws:iam::1111:saml-provider/IDP']) 27 | 28 | assert SamlAssertion(assertion).roles() == [{'role': 'arn:aws:iam::1111:role/DevRole', 29 | 'principle': 'arn:aws:iam::1111:saml-provider/IDP'}] 30 | 31 | def test_principle_can_be_first(self): 32 | assertion = saml_assertion(['arn:aws:iam::1111:saml-provider/IDP, arn:aws:iam::1111:role/DevRole']) 33 | 34 | assert SamlAssertion(assertion).roles() == [{'role': 'arn:aws:iam::1111:role/DevRole', 35 | 'principle': 'arn:aws:iam::1111:saml-provider/IDP'}] 36 | 37 | def test_white_space_is_removed(self): 38 | assertion = saml_assertion([' arn:aws:iam::1111:saml-provider/IDP , arn:aws:iam::1111:role/DevRole ']) 39 | 40 | assert SamlAssertion(assertion).roles() == [{'role': 'arn:aws:iam::1111:role/DevRole', 41 | 'principle': 'arn:aws:iam::1111:saml-provider/IDP'}] 42 | 43 | def test_multiple_roles_are_returned(self): 44 | assertion = saml_assertion(['arn:aws:iam::1111:role/DevRole,arn:aws:iam::1111:saml-provider/IDP', 45 | 'arn:aws:iam::2222:role/QARole,arn:aws:iam::2222:saml-provider/IDP']) 46 | 47 | assert SamlAssertion(assertion).roles() == [{'role': 'arn:aws:iam::1111:role/DevRole', 48 | 'principle': 'arn:aws:iam::1111:saml-provider/IDP'}, 49 | {'role': 'arn:aws:iam::2222:role/QARole', 50 | 'principle': 'arn:aws:iam::2222:saml-provider/IDP'}] 51 | 52 | def test_assertion_is_encoded(self): 53 | assert SamlAssertion("test encoding").encode() == b'dGVzdCBlbmNvZGluZw==' 54 | 55 | 56 | class TestAwsCredentialsFile(fake_filesystem_unittest.TestCase): 57 | TEST_FILE = "/test/file" 58 | 59 | def setUp(self): 60 | self.setUpPyfakefs() 61 | os.mkdir('/test') 62 | 63 | def tearDown(self): 64 | pass 65 | 66 | def test_profile_is_added(self): 67 | AwsCredentialsFile(self.TEST_FILE).add_profile( 68 | 'dev', 'un-west-5', Struct({'access_key': 'ACCESS_KEY', 69 | 'secret_key': 'SECRET_KEY', 70 | 'session_token': 'SESSION_TOKEN', 71 | 'expiration': 'TEST_EXPIRATION'})) 72 | 73 | six.assertCountEqual(self, read_config_file(self.TEST_FILE), 74 | ['[dev]', 75 | 'output = json', 76 | 'region = un-west-5', 77 | 'aws_access_key_id = ACCESS_KEY', 78 | 'aws_secret_access_key = SECRET_KEY', 79 | 'aws_security_token = SESSION_TOKEN', 80 | 'aws_session_token = SESSION_TOKEN', 81 | '']) 82 | 83 | def test_profile_is_updated(self): 84 | write_config_file(self.TEST_FILE, 85 | '[dev]', 86 | 'output = none', 87 | 'region = us-west-2', 88 | 'aws_access_key_id = OLD', 89 | 'aws_secret_access_key = REDUNDANT', 90 | 'aws_session_token = EXPIRED') 91 | 92 | AwsCredentialsFile(self.TEST_FILE).add_profile( 93 | 'dev', 'un-west-5', Struct({'access_key': 'ACCESS_KEY', 94 | 'secret_key': 'SECRET_KEY', 95 | 'session_token': 'SESSION_TOKEN', 96 | 'expiration': 'TEST_EXPIRATION'})) 97 | 98 | six.assertCountEqual(self, read_config_file(self.TEST_FILE), 99 | ['[dev]', 100 | 'region = un-west-5', 101 | 'aws_access_key_id = ACCESS_KEY', 102 | 'aws_secret_access_key = SECRET_KEY', 103 | 'output = json', 104 | 'aws_security_token = SESSION_TOKEN', 105 | 'aws_session_token = SESSION_TOKEN', 106 | '']) 107 | 108 | def test_existing_profiles_are_preserved(self): 109 | write_config_file(self.TEST_FILE, 110 | '[test]', 111 | 'output = none', 112 | 'region = us-west-2', 113 | 'aws_access_key_id = TEST_KEY', 114 | 'aws_secret_access_key = TEST_ACCESS', 115 | 'aws_security_token = TEST_TOKEN', 116 | 'aws_session_token = TEST_TOKEN') 117 | 118 | AwsCredentialsFile(self.TEST_FILE).add_profile( 119 | 'dev', 'un-west-5', Struct({'access_key': 'ACCESS_KEY', 120 | 'secret_key': 'SECRET_KEY', 121 | 'security_token': 'SESSION_TOKEN', 122 | 'session_token': 'SESSION_TOKEN', 123 | 'expiration': 'TEST_EXPIRATION'})) 124 | 125 | six.assertCountEqual(self, read_config_file(self.TEST_FILE), 126 | ['[test]', 127 | 'region = us-west-2', 128 | 'aws_access_key_id = TEST_KEY', 129 | 'aws_secret_access_key = TEST_ACCESS', 130 | 'output = none', 131 | 'aws_security_token = TEST_TOKEN', 132 | 'aws_session_token = TEST_TOKEN', 133 | '', 134 | '[dev]', 135 | 'output = json', 136 | 'region = un-west-5', 137 | 'aws_access_key_id = ACCESS_KEY', 138 | 'aws_secret_access_key = SECRET_KEY', 139 | 'aws_security_token = SESSION_TOKEN', 140 | 'aws_session_token = SESSION_TOKEN', 141 | '']) 142 | 143 | 144 | if __name__ == '__main__': 145 | sys.exit(unittest.main()) 146 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py33, py34 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir}:{toxinidir}/aws_role_credentials 7 | commands = python setup.py test 8 | 9 | ; If you want to make tox run the tests with the same versions, create a 10 | ; requirements.txt with the pinned versions and uncomment the following lines: 11 | ; deps = 12 | ; -r{toxinidir}/requirements.txt 13 | 14 | [flake8] 15 | max-line-length = 120 16 | --------------------------------------------------------------------------------