PKgˆG8úø@@mai/__main__.pyfrom mai.cli import main if __name__ == '__main__': main() PK4gˆGZ€oumai/__init__.py__version__ = '1.0.4' PKgˆG³!ކÿ&ÿ& mai/cli.pyimport click import os import keyring import yaml import aws_saml_login.saml import time import mai from aws_saml_login import authenticate, assume_role, write_aws_credentials from clickclick import Action, choice, error, AliasedGroup, info, print_table, OutputFormat CONFIG_DIR_PATH = click.get_app_dir('mai') CONFIG_FILE_PATH = os.path.join(CONFIG_DIR_PATH, 'mai.yaml') CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) def print_version(ctx, param, value): if not value or ctx.resilient_parsing: return click.echo('Mai {}'.format(mai.__version__)) ctx.exit() output_option = click.option('-o', '--output', type=click.Choice(['text', 'json', 'tsv']), default='text', help='Use alternative output format') @click.group(cls=AliasedGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS) @click.option('--config-file', '-c', help='Use alternative configuration file', default=CONFIG_FILE_PATH, metavar='PATH') @click.option('-V', '--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True, help='Print the current version number and exit.') @click.option('--awsprofile', help='Profilename in ~/.aws/credentials', default='default', show_default=True) @click.pass_context def cli(ctx, config_file, awsprofile): path = os.path.abspath(os.path.expanduser(config_file)) data = {} if os.path.exists(path): with open(path, 'rb') as fd: data = yaml.safe_load(fd) ctx.obj = {'config': data, 'config-file': path, 'config-dir': os.path.dirname(path), 'last-update-filename': os.path.join(os.path.dirname(path), 'last_update.yaml')} if not ctx.invoked_subcommand: if not data: raise click.UsageError('No profile configured. Use "mai create .." to create a new profile.') profile = None if 'global' in data: profile = data['global'].get('default_profile') if not profile: profile = sorted([k for k in data.keys() if k != 'global'])[0] login_with_profile(ctx.obj, profile, data.get(profile), awsprofile) @cli.command('list') @output_option @click.pass_obj def list_profiles(obj, output): '''List profiles''' if obj['config']: rows = [] for name, config in obj['config'].items(): row = { 'name': name, 'role': get_role_label(config.get('saml_role')), 'url': config.get('saml_identity_provider_url'), 'user': config.get('saml_user')} rows.append(row) rows.sort(key=lambda r: r['name']) with OutputFormat(output): print_table(sorted(rows[0].keys()), rows) def get_role_label(role): """ >>> get_role_label(('arn:aws:iam::123:saml-provider/Shibboleth',\ 'arn:aws:iam::123:role/Shibboleth-PowerUser', 'zalando-stups')) 'AWS Account 123 (zalando-stups): Shibboleth-PowerUser' """ if not role: return '' provider_arn, role_arn, name = role number = role_arn.split(':')[4] return 'AWS Account {} ({}): {}'.format(number, name, role_arn.split('/')[-1]) @cli.command() @click.argument('profile-name') @click.option('--url', prompt='Identity provider URL') @click.option('-U', '--user', envvar='SAML_USER', prompt='SAML username') @click.pass_obj def create(obj, profile_name, url, user): '''Create a new profile''' if not url.startswith('http'): url = 'https://{}'.format(url) saml_xml, roles = saml_login(user, url) if not roles: error('No roles found') exit(1) if len(roles) == 1: role = roles[0] if role[2] is None: role = (role[0], role[1], profile_name) else: role = choice('Please select one role', [(r, get_role_label(r)) for r in sorted(roles)]) data = obj['config'] if not data: data = {} data[profile_name] = { 'saml_identity_provider_url': url, 'saml_role': role, 'saml_user': user } path = obj['config-file'] with Action('Storing new profile in {}..'.format(path)): os.makedirs(obj['config-dir'], exist_ok=True) with open(path, 'w') as fd: yaml.safe_dump(data, fd) @cli.command('create-all') @click.option('--url', prompt='Identity provider URL') @click.option('-U', '--user', envvar='SAML_USER', prompt='SAML username') @click.pass_obj def create_all(obj, url, user): '''Create for all roles a new own profile''' if not url.startswith('http'): url = 'https://{}'.format(url) saml_xml, roles = saml_login(user, url) if not roles: error('No roles found') exit(1) data = obj['config'] if not data: data = {} if len(roles) == 1: if roles[0][2] is None: roles = [(roles[0][0], roles[0][1], 'default')] for r in sorted(roles): provider_arn, role_arn, name = r name = name or 'unknown' # name is sometimes missing profile_name = '{}-{}'.format(name.split('-', maxsplit=1)[-1], role_arn.split('-', maxsplit=1)[-1]) data[profile_name] = { 'saml_identity_provider_url': url, 'saml_role': r, 'saml_user': user } path = obj['config-file'] with Action('Storing new profile in {}..'.format(path)): os.makedirs(obj['config-dir'], exist_ok=True) with open(path, 'w') as fd: yaml.safe_dump(data, fd) @cli.command('set-default') @click.argument('profile-name') @click.pass_obj def set_default(obj, profile_name): '''Set default profile''' data = obj['config'] if not data or profile_name not in data: raise click.UsageError('Profile "{}" does not exist'.format(profile_name)) data['global'] = { 'default_profile': profile_name } path = obj['config-file'] with Action('Storing configuration in {}..'.format(path)): os.makedirs(obj['config-dir'], exist_ok=True) with open(path, 'w') as fd: yaml.safe_dump(data, fd) def saml_login(user, url): ring_user = '{}@{}'.format(user, url) saml_password = keyring.get_password('mai', ring_user) saml_xml = None while not saml_xml: if not saml_password: saml_password = click.prompt('Please enter your SAML password', hide_input=True) with Action('Authenticating against {url}..', url=url) as act: try: saml_xml, roles = authenticate(url, user, saml_password) except aws_saml_login.saml.AuthenticationFailed: act.error('Authentication Failed') info('Please check your username/password and try again.') saml_password = None keyring.set_password('mai', ring_user, saml_password) return saml_xml, roles def login_with_profile(obj, profile, config, awsprofile): url = config.get('saml_identity_provider_url') user = config.get('saml_user') role = config.get('saml_role') if not url: raise click.UsageError('Missing identity provider URL') if not user: raise click.UsageError('Missing SAML username') saml_xml, roles = saml_login(user, url) with Action('Assuming role {role}..', role=get_role_label(role)): key_id, secret, session_token = assume_role(saml_xml, role[0], role[1]) with Action('Writing temporary AWS credentials..'): write_aws_credentials(awsprofile, key_id, secret, session_token) with open(obj['last-update-filename'], 'w') as fd: yaml.safe_dump({'timestamp': time.time(), 'profile': profile}, fd) @cli.command('delete') @click.argument('profile-name') @click.pass_obj def delete(obj, profile_name): '''Delete profile''' path = obj['config-file'] if not obj['config'] or profile_name not in obj['config']: raise click.UsageError('Profile "{}" does not exist'.format(profile_name)) del obj['config'][profile_name] with Action('Deleting profile from {}..'.format(path)): os.makedirs(obj['config-dir'], exist_ok=True) with open(path, 'w') as fd: yaml.safe_dump(obj['config'], fd) @cli.command() @click.argument('profile', nargs=-1) @click.option('-r', '--refresh', is_flag=True, help='Keep running and refresh access tokens automatically') @click.option('--awsprofile', help='Profilename in ~/.aws/credentials', default='default', show_default=True) @click.pass_obj def login(obj, profile, refresh, awsprofile): '''Login with given profile(s)''' repeat = True while repeat: last_update = get_last_update(obj) if 'profile' in last_update and last_update['profile'] and not profile: profile = [last_update['profile']] for prof in profile: if prof not in obj['config']: raise click.UsageError('Profile "{}" does not exist'.format(prof)) login_with_profile(obj, prof, obj['config'][prof], awsprofile) if refresh: last_update = get_last_update(obj) wait_time = 3600 * 0.9 with Action('Waiting {} minutes before refreshing credentials..' .format(round(((last_update['timestamp']+wait_time)-time.time()) / 60))) as act: while time.time() < last_update['timestamp'] + wait_time: try: time.sleep(120) except KeyboardInterrupt: # do not show "EXCEPTION OCCURRED" for CTRL+C repeat = False break act.progress() else: repeat = False def get_last_update(obj): try: with open(obj['last-update-filename'], 'rb') as fd: last_update = yaml.safe_load(fd) except: last_update = {'timestamp': 0} return last_update def main(): cli() PK4gˆG&ÒãÕÕ)stups_mai-1.0.4.dist-info/DESCRIPTION.rst=== Mai === .. image:: https://travis-ci.org/zalando-stups/mai.svg?branch=master :target: https://travis-ci.org/zalando-stups/mai :alt: Build Status .. image:: https://coveralls.io/repos/zalando-stups/mai/badge.svg :target: https://coveralls.io/r/zalando-stups/mai :alt: Code Coverage .. image:: https://img.shields.io/pypi/dw/stups-mai.svg :target: https://pypi.python.org/pypi/stups-mai/ :alt: PyPI Downloads .. image:: https://img.shields.io/pypi/v/stups-mai.svg :target: https://pypi.python.org/pypi/stups-mai/ :alt: Latest PyPI version .. image:: https://img.shields.io/pypi/l/stups-mai.svg :target: https://pypi.python.org/pypi/stups-mai/ :alt: License AWS SAML login command line utility. .. code-block:: bash $ sudo pip3 install --upgrade stups-mai Usage ===== .. code-block:: bash $ mai create my-profile $ mai # short for "mai login my-profile" See the `STUPS documentation on Mai`_ for details. Running Unit Tests ================== .. code-block:: bash $ python3 setup.py test --cov-html=true .. _STUPS documentation on Mai: http://stups.readthedocs.org/en/latest/components/mai.html Releasing ========= .. code-block:: bash $ ./release.sh PK4gˆGàm¼&&*stups_mai-1.0.4.dist-info/entry_points.txt[console_scripts] mai = mai.cli:main PK4gˆG(•@»§§'stups_mai-1.0.4.dist-info/metadata.json{"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython"], "extensions": {"python.commands": {"wrap_console": {"mai": "mai.cli:main"}}, "python.details": {"contacts": [{"email": "henning.jacobs@zalando.de", "name": "Henning Jacobs", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/zalando-stups/mai"}}, "python.exports": {"console_scripts": {"mai": "mai.cli:main"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["aws", "account", "saml", "login", "federated", "shibboleth"], "license": "Apache License 2.0", "metadata_version": "2.0", "name": "stups-mai", "run_requires": [{"requires": ["PyYAML", "aws-saml-login (>=0.9)", "clickclick (>=0.6)", "keyring"]}], "summary": "AWS SAML login CLI", "test_requires": [{"requires": ["pytest", "pytest-cov"]}], "version": "1.0.4"}PK4gˆG%h÷õ'stups_mai-1.0.4.dist-info/top_level.txtmai PK4gˆG}À‚¼\\stups_mai-1.0.4.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py3-none-any PK4gˆG #US33"stups_mai-1.0.4.dist-info/METADATAMetadata-Version: 2.0 Name: stups-mai Version: 1.0.4 Summary: AWS SAML login CLI Home-page: https://github.com/zalando-stups/mai Author: Henning Jacobs Author-email: henning.jacobs@zalando.de License: Apache License 2.0 Keywords: aws account saml login federated shibboleth Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Dist: PyYAML Requires-Dist: aws-saml-login (>=0.9) Requires-Dist: clickclick (>=0.6) Requires-Dist: keyring === Mai === .. image:: https://travis-ci.org/zalando-stups/mai.svg?branch=master :target: https://travis-ci.org/zalando-stups/mai :alt: Build Status .. image:: https://coveralls.io/repos/zalando-stups/mai/badge.svg :target: https://coveralls.io/r/zalando-stups/mai :alt: Code Coverage .. image:: https://img.shields.io/pypi/dw/stups-mai.svg :target: https://pypi.python.org/pypi/stups-mai/ :alt: PyPI Downloads .. image:: https://img.shields.io/pypi/v/stups-mai.svg :target: https://pypi.python.org/pypi/stups-mai/ :alt: Latest PyPI version .. image:: https://img.shields.io/pypi/l/stups-mai.svg :target: https://pypi.python.org/pypi/stups-mai/ :alt: License AWS SAML login command line utility. .. code-block:: bash $ sudo pip3 install --upgrade stups-mai Usage ===== .. code-block:: bash $ mai create my-profile $ mai # short for "mai login my-profile" See the `STUPS documentation on Mai`_ for details. Running Unit Tests ================== .. code-block:: bash $ python3 setup.py test --cov-html=true .. _STUPS documentation on Mai: http://stups.readthedocs.org/en/latest/components/mai.html Releasing ========= .. code-block:: bash $ ./release.sh PK4gˆGC‘‡ -- stups_mai-1.0.4.dist-info/RECORDmai/__init__.py,sha256=thGZHY6pLPhJc6CwYBjUaQ6pGxLhwPMp_pjoYsiwvI4,22 mai/__main__.py,sha256=Nr3hLFNjwXrwsg3vtv18pM1cyMTyh8-85I-PZi7pHAc,64 mai/cli.py,sha256=bDIYR5XJwnVgDeQrBuHgu0YaBqC3NYRw2OjelPZ1s3k,9983 stups_mai-1.0.4.dist-info/DESCRIPTION.rst,sha256=s7IwbXb3SzRHaWRLPM3peUpyXzmDxThVATFg86m16DY,1237 stups_mai-1.0.4.dist-info/METADATA,sha256=p5zcgvLETyZX_x5t0ql0RYUHJTKdrnLmEAuVQK9t8UY,2099 stups_mai-1.0.4.dist-info/RECORD,, stups_mai-1.0.4.dist-info/WHEEL,sha256=zX7PHtH_7K-lEzyK75et0UBa3Bj8egCBMXe1M4gc6SU,92 stups_mai-1.0.4.dist-info/entry_points.txt,sha256=kuVUQJhYrmdbH-067RkoyoXQeSWJey1NoeOnEg2dS9w,38 stups_mai-1.0.4.dist-info/metadata.json,sha256=fuChHzolhQ7btf6d4qzm0KqyPjKj8IsEcVt6mpFgZz0,1191 stups_mai-1.0.4.dist-info/top_level.txt,sha256=DuhBUF7h5ssR4rZqHToCX5ajAurkzEvg9pCAGem3KxQ,4 PKgˆG8úø@@mai/__main__.pyPK4gˆGZ€oummai/__init__.pyPKgˆG³!ކÿ&ÿ& °mai/cli.pyPK4gˆG&ÒãÕÕ)×'stups_mai-1.0.4.dist-info/DESCRIPTION.rstPK4gˆGàm¼&&*ó,stups_mai-1.0.4.dist-info/entry_points.txtPK4gˆG(•@»§§'a-stups_mai-1.0.4.dist-info/metadata.jsonPK4gˆG%h÷õ'M2stups_mai-1.0.4.dist-info/top_level.txtPK4gˆG}À‚¼\\–2stups_mai-1.0.4.dist-info/WHEELPK4gˆG #US33"/3stups_mai-1.0.4.dist-info/METADATAPK4gˆGC‘‡ -- ¢;stups_mai-1.0.4.dist-info/RECORDPK ö ?