diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index d7d4ee0..0000000 --- a/.coveragerc +++ /dev/null @@ -1,6 +0,0 @@ -[run] -include = - awsume/autoawsume.py - awsume/awsumepy.py -[report] -show_missing = True diff --git a/.gitignore b/.gitignore index 0dd4a21..974fe44 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# misc +*.local + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -109,7 +112,7 @@ Pipfile.lock # mypy .mypy_cache/ -# vscode +# vscode .vscode # sandbox files diff --git a/MANIFEST.in b/MANIFEST.in index edd95e7..93aa813 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,3 @@ -include package.json \ No newline at end of file +include readme.md +include fastentrypoints.py +include autocomplete.py diff --git a/Pipfile b/Pipfile index fcf45d6..12a6934 100644 --- a/Pipfile +++ b/Pipfile @@ -1,20 +1,22 @@ [[source]] -url = "https://pypi.python.org/simple" -verify_ssl = true name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true [dev-packages] twine = "*" +pylint = "*" +botostubs = "*" [packages] -pytest = "*" -pytest-cov = "*" -mock = "*" -python-dateutil = "*" -"boto3" = "*" +pluggy = "*" +boto3 = "*" +awsume = {editable = true,path = "."} psutil = "*" -yapsy = "*" -future = "*" -pylint = "*" colorama = "*" -awsume = {editable = true,path = "."} + +[scripts] +test = "pytest" +build = "python setup.py sdist" +deploy-test = "twine upload -r test dist/*" +deploy-pypi = "twine upload -r pypi dist/*" diff --git a/README.md b/README.md index 3f0b96b..27e6555 100644 --- a/README.md +++ b/README.md @@ -1,131 +1,3 @@ # AWSume: AWS Assume Made Awesome -Utility for easily assuming AWS IAM roles from the command line, now in Python! - -## What is AWSume? - -AWSume is a cross-platform (Mac, Linux, Windows) command-line tool that makes assuming AWS roles and setting user credentials from the AWS CLI easy! It works by scanning your `.aws/config` and `.aws/credentials` files for the profile you give it, making AWS calls to get that profile's credentials, and exporting those credentials to your shell's environment variables. Then, any AWS CLI calls you make in that shell will be under the profile you gave AWSume. - -## Installation - -### Pip Installation - -AWSume has been conveniently wrapped into a Python package and installable with just one simple command: - -``` bash -pip install awsume -``` - -The installer places the python and shell scripts into your python directory. If you're using `Bash` or `Zsh`, the installer will add an alias definition (sources awsume when it's called) to their resource control file, either `.bash_alias`, `.bashrc`, `.bash_profile`, or `.zshrc`. When uninstalling AWSume, the alias definition will not be removed. - -Once you have AWSume installed, you're ready to set up AWSume! - -#### Console Plugin installation - -Once you've installed AWSume, you can install the console plugin with: - -``` bash -awsume --install-plugin https://raw.githubusercontent.com/trek10inc/awsume/master/examplePlugin/console.py https://raw.githubusercontent.com/trek10inc/awsume/master/examplePlugin/console.yapsy-plugin -``` - -## Setup - -### Configuring Using The AWS CLI - -`aws configure set --profile ` - -Where: - -- `key` is what you would like to set within the `config`/`credentials` file, such as: - - `aws_access_key_id`, `aws_secret_access_key`, `region`, `output`, `mfa_serial`, `role_arn`, or `source_profile` -- `value` is the value you'd like to set the `key` to -- `profile_name` is the name of the profile you are creating - - `profile_name` is what you will pass into AWSume - -### Configuring Manually - -Add profiles to - -`~/.aws/config` (for macOS / Linux) - -`%userprofile%\.aws\config` (for Windows) - -#### ~/.aws/config - -``` ini -[default] -region = us-east-1 -[profile internal-admin] -role_arn = arn:aws:iam:::role/admin-role -source_profile = joel -region = us-east-1 -[profile client1-admin] -role_arn = arn:aws:iam:::role/admin-role -mfa_serial = arn:aws:iam:::mfa/joel -source_profile = joel -region = us-west-2 -[profile client2-admin] -role_arn = arn:aws:iam:::role/admin-role -mfa_serial = arn:aws:iam:::mfa/joel -source_profile = joel -region = us-east-1 -``` - -Add credentials to - -`~/.aws/credentials` (for macOS / Linux) - -`%userprofile%\.aws\credentials` (for Windows) - -#### ~/.aws/credentials - -``` ini -[default] -aws_access_key_id = AKIAIOIEUFSN9EXAMPLE -aws_secret_access_key = wJalrXIneUATF/K7MDENG/jeuFHEnfEXAMPLEKEY -[joel] -aws_access_key_id = AKIAIOSFODNN7EXAMPLE -aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -``` - -### Plugins - -AWSume is now extensible. It now comes with a built-in plugin manager! To get started developing plugins, check out our [plugin documentation](https://github.com/trek10inc/awsume/wiki/Plugins). - -#### AWSume Console Plugin - -To demonstrate the plugin manager, we've extended the functionality of AWSume through the AWSume Console plugin. This plugin will open the AWS console to the assumed role. Read about it [here](https://github.com/trek10inc/awsume/blob/master/examplePlugin/console.md) - -### Example Usages - -`awsume client1-source-profile` -Exports `client1-source-profile` credentials into current shell, will ask for MFA if needed - -`awsume client1-source-profile -n` -Exports `client1-source-profile` credentials into current shell, will usually not ask for MFA, but it will if `client1-source-profile` is a role profile instead of a source profile, and requires MFA - -`awsume client1-admin` -Exports `client1-admin` credentials into current shell, will ask for MFA if needed - -`awsume` -Exports the default profile's credentials into current shell, will ask for MFA if needed - -`awsume -d` -Exports the default profile's credentials into current shell, will ask for MFA if needed - -`awsume client1-admin -s` -Outputs export commands to shell, useful if you want to copy / paste into some other shell, will ask for MFA if needed - -`awsume client1-admin -r` -Delete cached credentials and refresh, will always prompt for MFA. - -`awsume client1-admin -a` -Exports auto-refresh profile to shell's `AWS_DEFAULT_PROFILE` and `AWS_PROFILE` environment variables, creates a profile in the `.aws/credentials` file called `auto-refresh-client1-admin` that contains profile's role credentials, and spawns a background process to auto-refresh those role credentials when they expire, for as long as the role's source profile is valid. - -`awsume client1-admin -k` -Removes the `auto-refresh-client1-admin` profile from the `.aws/credentials` file. If no more `auto-refresh-` profiles are left in the `.aws/credentials` file, the auto-refreshing background process will be killed. - -`awsume -k` -Removes all `auto-refresh-` profiles from the `.aws/credentials` file, and kills the auto-refreshing background process. - -See our blog posts [AWSume](https://www.trek10.com/blog/awsume-aws-assume-made-awesome/) and [AWSume - Now in Python](https://www.trek10.com/blog/awsume-now-in-python/) for more details. +Awsume v4 is a work in progress. \ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 100644 index 1d73fc3..0000000 --- a/README.rst +++ /dev/null @@ -1,4 +0,0 @@ -AWSume: AWS Assume Made Awesome -=============================== - -Visit our `GitHub `_ \ No newline at end of file diff --git a/awsume/__data__.py b/awsume/__data__.py new file mode 100644 index 0000000..0482e0c --- /dev/null +++ b/awsume/__data__.py @@ -0,0 +1,10 @@ +version = '0.0.3' + +name = 'awsume' +author = 'Trek10, Inc' +author_email = 'package-management@trek10.com' +description = 'Awsume - A cli that makes using AWS IAM credentials easy' +license = 'MIT' +homepage = 'https://github.com/trek10inc/awsume' + +message = 'Thank you for using AWSume! Check us out at https://trek10.com' diff --git a/awsume/__init__.py b/awsume/__init__.py index e69de29..b784ba9 100644 --- a/awsume/__init__.py +++ b/awsume/__init__.py @@ -0,0 +1,12 @@ +from . import * +from . import __data__ +from . awsumepy import safe_print + +__VERSION__ = __data__.version +__NAME__ = __data__.name +__AUTHOR__ = __data__.author +__AUTHOR_EMAIL__ = __data__.author_email +__DESCRIPTION__ = __data__.description +__LICENSE__ = __data__.license +__HOMEPAGE__ = __data__.homepage +__MESSAGE__ = __data__.message diff --git a/awsume/autoawsume.py b/awsume/autoawsume.py deleted file mode 100644 index 809727e..0000000 --- a/awsume/autoawsume.py +++ /dev/null @@ -1,118 +0,0 @@ -"""autoawsume - A daemon to auto refresh 'auto-refresh' profiles in the credentials file""" -from __future__ import print_function -import sys -import datetime -import time -import botocore -import dateutil - -from awsume import awsumepy -from awsume.awsumepy import AWS_CACHE_DIRECTORY, AWS_CREDENTIALS_FILE - -def get_now(): # pragma: no cover - """Return datetime.datetime.now().""" - return datetime.datetime.now() - -def refresh_session(autoProfile): - """Refresh the `oldSession` role credentials. - - Parameters - ---------- - - oldSession - the session to refresh; - - roleArn - the role_arn used to make the assume_role call; - - sessionName - what to name the assumed role session; - - Returns - ------- - The refreshed role session - """ - sourceCredentials = awsumepy.read_aws_cache(AWS_CACHE_DIRECTORY, autoProfile['awsume_cache_name']) - stsClient = awsumepy.create_sts_client(sourceCredentials['AccessKeyId'], - sourceCredentials['SecretAccessKey'], - sourceCredentials['SessionToken']) - try: - response = stsClient.assume_role(RoleArn=autoProfile['aws_role_arn'], RoleSessionName=autoProfile['awsume_session_name']) - session = response['Credentials'] - session['Expiration'] = session['Expiration'].astimezone(dateutil.tz.tzlocal()) - session['Expiration'] = session['Expiration'].strftime('%Y-%m-%d %H:%M:%S') - session['region'] = sourceCredentials['region'] - - autoProfile['aws_access_key_id'] = session['AccessKeyId'] - autoProfile['aws_secret_access_key'] = session['SecretAccessKey'] - autoProfile['aws_session_token'] = session['SessionToken'] - autoProfile['awsume_role_expiration'] = session['Expiration'] - awsumepy.write_auto_awsume_session(autoProfile['__name__'].replace('auto-refresh-', ''), autoProfile, AWS_CREDENTIALS_FILE) - except botocore.exceptions.ClientError: - pass - -def extract_auto_refresh_profiles(profiles): - """Pull out any profiles with the prefix 'auto-refresh-' in the name. - - Parameters - ---------- - - profiles - the profiles read from the aws credentials file - - Returns - ------- - A dict of profiles that are prefixed by 'auto-refresh-' in the name. - """ - autoRefreshProfiles = {} - for profile in profiles: - if 'auto-refresh-' in profile: - autoRefreshProfiles[profile] = profiles[profile] - return autoRefreshProfiles - -def get_earliest_expiration(autoProfiles): - """Get the earliest expiration from the autoProfiles - - Parameters - ---------- - - autoProfiles - the autoawsume profiles from the credentials profile - - Returns - ------- - A datetime object containing the earliest expiration. - """ - expirations = [] - for profile in autoProfiles: - expirations.append( - datetime.datetime.strptime( - autoProfiles[profile]['awsume_user_expiration'], '%Y-%m-%d %H:%M:%S')) - expirations.append( - datetime.datetime.strptime( - autoProfiles[profile]['awsume_role_expiration'], '%Y-%m-%d %H:%M:%S')) - if expirations: - return min(expirations) - else: - return get_now() - -def refresh_expired_profiles(autoProfiles): - """Refresh any expired autoProfiles. - - Parameters - ---------- - - autoProfiles - the autoawsume profiles from the credentials profile - """ - for profile in autoProfiles: - userExpiration = datetime.datetime.strptime(autoProfiles[profile]['awsume_user_expiration'], '%Y-%m-%d %H:%M:%S') - roleExpiration = datetime.datetime.strptime(autoProfiles[profile]['awsume_role_expiration'], '%Y-%m-%d %H:%M:%S') - if roleExpiration < get_now(): - refresh_session(autoProfiles[profile]) - if userExpiration < get_now(): - awsumepy.remove_auto_profile(autoProfiles[profile]['__name__'].replace('auto-refresh-', '')) - -def main(): - while True: - credentialsProfiles = awsumepy.read_ini_file(AWS_CREDENTIALS_FILE) - autoRefreshProfiles = extract_auto_refresh_profiles(credentialsProfiles) - refresh_expired_profiles(autoRefreshProfiles) - earliestExpiration = get_earliest_expiration(autoRefreshProfiles) - timeUntilEarliestExpiration = (earliestExpiration - get_now().replace(tzinfo=earliestExpiration.tzinfo)).total_seconds() - if timeUntilEarliestExpiration <= 0: - break - # awsumepy.safe_print("autoawsume: Sleeping for " + str(timeUntilEarliestExpiration) + " seconds", file=sys.stderr) - time.sleep(timeUntilEarliestExpiration) - # awsumepy.safe_print("autoawsume: No more credentials left to refresh, shutting down", file=sys.stderr) - -if __name__ == '__main__': # pragma: no cover - main() diff --git a/awsume/autoawsume/__init__.py b/awsume/autoawsume/__init__.py new file mode 100644 index 0000000..b974282 --- /dev/null +++ b/awsume/autoawsume/__init__.py @@ -0,0 +1 @@ +from . import * \ No newline at end of file diff --git a/awsume/autoawsume/main.py b/awsume/autoawsume/main.py new file mode 100644 index 0000000..8e00653 --- /dev/null +++ b/awsume/autoawsume/main.py @@ -0,0 +1,42 @@ +import json +import subprocess +import configparser +import time +from datetime import datetime, timedelta + +from ..awsumepy.main import run_awsume +from ..awsumepy.lib.aws_files import get_aws_files, delete_section + + +def main(): + _, credentials_file = get_aws_files(None, None) + while True: + credentials = configparser.ConfigParser() + credentials.read(credentials_file) + auto_profiles = {k: dict(v) for k, v in credentials._sections.items() if k.startswith('autoawsume-')} + + expirations = [] + for auto_profile_name, auto_profile in auto_profiles.items(): + expiration = datetime.strptime(auto_profile['expiration'], '%Y-%m-%d %H:%M:%S') + source_expiration = datetime.strptime(auto_profile['source_expiration'], '%Y-%m-%d %H:%M:%S') + + if expiration < datetime.now() and source_expiration < datetime.now(): + print('Source credentials are expired, removing autoawsume profile') + delete_section(auto_profile_name, credentials_file) + continue + + if expiration < datetime.now() + timedelta(minutes=5): + print('Refreshing {}'.format(auto_profile_name)) + subprocess.run(auto_profile.get('awsumepy_command').split(' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + expirations.append(datetime.now() + timedelta(hours=1)) + else: + expirations.append(expiration) + + if not expirations: + break + + earliest_expiration = min(expirations) + time_to_sleep = (earliest_expiration - datetime.now().replace(tzinfo=earliest_expiration.tzinfo)).total_seconds() + + print('sleeping for {}'.format(time_to_sleep)) + time.sleep(time_to_sleep) diff --git a/awsume/autoawsume/process.py b/awsume/autoawsume/process.py new file mode 100644 index 0000000..e4f4cbe --- /dev/null +++ b/awsume/autoawsume/process.py @@ -0,0 +1,38 @@ +import argparse +import psutil + +from ..awsumepy.lib.aws_files import delete_section, get_aws_files, read_aws_file +from ..awsumepy.lib.logger import logger + +def kill_autoawsume(): + logger.debug('Killing autoawsume') + for proc in psutil.process_iter(): + try: + for command_string in proc.cmdline(): + if 'autoawsume' in command_string: + proc.kill() + except Exception: + pass + + +def kill(arguments: argparse.Namespace): + _, credentials_file = get_aws_files(None, None) + if arguments.profile_name: + logger.debug('Stoping auto-refresh of profile {}'.format(arguments.profile_name)) + delete_section('autoawsume-{}'.format(arguments.profile_name), credentials_file) + profiles = read_aws_file(credentials_file) + profile_names = [_ for _ in profiles] + if any(['autoawsume-' in _ for _ in profile_names]): + print('Stop {}'.format(arguments.profile_name)) + return + else: + logger.debug('There were not more autoawsume profiles, stopping autoawsume') + kill_autoawsume() + else: + logger.debug('Stopping all auto refreshing and removing autoawsume profiles') + kill_autoawsume() + profiles = read_aws_file(credentials_file) + for profile in profiles: + if 'autoawsume-' in profile: + delete_section(profile, credentials_file) + print('Kill') diff --git a/awsume/awsumepy.py b/awsume/awsumepy.py deleted file mode 100644 index 799eb22..0000000 --- a/awsume/awsumepy.py +++ /dev/null @@ -1,1706 +0,0 @@ -"""Awsume - A cli that makes using AWS IAM credentials easy""" -from __future__ import print_function -import sys -import os -import argparse -import re -import collections -import logging -import json -import signal -import shutil -from datetime import datetime -from builtins import input as read_input -import boto3 -import botocore -import psutil -import dateutil -import pkg_resources -import six -from colorama import Fore, Style -from six.moves import configparser as ConfigParser -from yapsy import PluginManager - -try: # pragma: no cover - __version__ = pkg_resources.get_distribution('awsume').version -except Exception: # pragma: no cover - pass - -# remove traceback on ctrl+C -def __exit_awsume(arg1, arg2): # pragma: no cover - """Make sure ^C doesn't spam the terminal. - Ignore `arg1` and `arg2`.""" - print('') - sys.exit(0) -signal.signal(signal.SIGINT, __exit_awsume) - -#initialize logging -# logging.getLogger('yapsy').addHandler(logging.StreamHandler()) -LOG = logging.getLogger(__name__) -LOG_HANDLER = logging.StreamHandler() -LOG_HANDLER.setFormatter(logging.Formatter('%(name)s.%(funcName)s : %(message)s')) -LOG.addHandler(LOG_HANDLER) - -#get cross-platform directories -HOME_PATH = os.path.expanduser('~') -AWS_DIRECTORY = os.path.join(HOME_PATH, '.aws') -AWS_CONFIG_FILE = os.path.join(AWS_DIRECTORY, 'config') -AWS_CREDENTIALS_FILE = os.path.join(AWS_DIRECTORY, 'credentials') -AWS_CACHE_DIRECTORY = os.path.join(AWS_DIRECTORY, 'cli/cache/') -AWSUME_PLUGIN_DIRECTORY = os.path.join(AWS_DIRECTORY, 'awsumePlugins/') -AWSUME_PLUGIN_CACHE_FILE = os.path.join(AWSUME_PLUGIN_DIRECTORY, '_plugins.json') -AWSUME_OPTIONS_FILE = os.path.join(AWS_DIRECTORY, 'awsume.json') - -# AWSume options -AWSUME_OPTIONS = {} - - - -# -# Exceptions -# -class ProfileNotFoundError(Exception): - """Error that should be raised when no profile is found. - It should only be raised after all `get_aws_profiles` functions have been called.""" -class InvalidProfileError(Exception): - """Error that should be raised when the targeted profile is invalid in some way.""" -class UserAuthenticationError(Exception): - """Error that should be raised when AWSume failed to authenticate user.""" -class RoleAuthenticationError(Exception): - """Error that should be raised when AWSume failed to authenticate the role.""" - - - -# -# CommandLineArgumentHandling -# -def custom_duration(string): - """""" - number = int(string) - if number >= 0 and number <= 43201: - return number - raise argparse.ArgumentTypeError('Custom Duration must be between 0 and 43200') - -def generate_argument_parser(): - """Create the argparse argument parser. - - Returns - ------- - An `argparse` ArgumentParser - """ - epilog = """Thank you for using AWSume! Check us out at https://trek10.com""" - return argparse.ArgumentParser(description=__doc__, - epilog=epilog, - formatter_class=lambda prog: (argparse.RawDescriptionHelpFormatter(prog, max_help_position=50))) - -def add_arguments(argument_parser): - """Add all of awsume's arguments to the argument parser. - - Parameters - ---------- - - argument_parser - the main argument parser for awsume - - Returns - ------- - The argument parser with the arguments added to it. - """ - argument_parser.add_argument('-v', '--version', - action='store_true', - default=False, - dest='version', - help='Display the current version of AWSume') - argument_parser.add_argument(action='store', - dest='profile_name', - nargs='?', - metavar='profile_name', - help='The target profile name') - argument_parser.add_argument('-s', - action='store_true', - dest='show_commands', - default=False, - help='Show the commands to set the credentials') - argument_parser.add_argument('-r', '--refresh', - action='store_true', - dest='force_refresh', - default=False, - help='Force refresh credentials') - argument_parser.add_argument('-u', '--unset', - action='store_true', - dest='unset_variables', - default=False, - help='Unset awsume\'s environment variables') - argument_parser.add_argument('-l', '--list', - action='store_true', - default=False, - dest='list_profiles', - help='List information about your profiles') - argument_parser.add_argument('--session-name', - default=None, - dest='session_name', - metavar='session_name', - help='Set a custom session name') - argument_parser.add_argument('--install-plugin', - nargs=2, - dest='plugin_urls', - metavar=('.py_url', '.yapsy-plugin_url'), - default=None, - help='Install a plugin given two urls') - argument_parser.add_argument('--delete-plugin', - nargs=1, - dest='delete_plugin_name', - metavar=('name_of_plugin'), - default=None, - help='Delete the .py and .yapsy-plugin files of the given plugin') - argument_parser.add_argument('--rolesusers', - action='store_true', - default=False, - dest='list_profile_names', - help='List all profile names available') - argument_parser.add_argument('--plugin-info', - action='store_true', - default=False, - dest='display_plugin_info', - help='Display information about installed plugins') - argument_parser.add_argument('-a', '--auto-refresh', - action='store_true', - default=False, - dest='auto_refresh', - help='Auto-refresh role credentials') - argument_parser.add_argument('-k', '--kill-refreshing', - action='store_true', - default=False, - dest='kill', - help='Kill autoawsume') - argument_parser.add_argument('--config', - nargs=2, - dest='config', - metavar=('option-name', 'option-value'), - default=None, - help='Configure AWSume\'s settings') - argument_parser.add_argument('--config-help', - action='store_true', - default=False, - dest='config_help', - help='Display info on AWSume\'s settings') - argument_parser.add_argument('--role-duration', - type=custom_duration, - dest='role_duration', - metavar=('duration_seconds'), - help='The duration to use when calling assume-role') - argument_parser.add_argument('--mfa-token', - dest='mfa_token', - help='MFA token to use. Will prompt if not given.') - argument_parser.add_argument('--info', - action='store_true', - dest='info', - help='Print any info logs to stderr') - argument_parser.add_argument('--debug', - action='store_true', - dest='debug', - help='Print any debug logs to stderr') - return argument_parser - -def parse_args(argument_parser, system_arguments): - """Call `parse_args` on the argument parser. - - Parameters - ---------- - - argument_parser - the main argument parser for awsume - - system_arguments - the arguments from the system - - Returns - ------- - The parsed arguments. - """ - return argument_parser.parse_args(system_arguments) - - - -# -# ReadAWSFiles -# -def read_ini_file(file_path): - """Read an ini file and return the profile data. - If the profile name begins with 'profile ', remove it. - - Parameters - ---------- - - file_path - the path to the file to read - - Returns - ------- - The profile data. - """ - LOG.info('Reading ini file from %s', file_path) - - profiles = {} - if os.path.exists(file_path): - parser = ConfigParser.ConfigParser() - parser.read(file_path) - for profile in parser.sections(): - profiles[profile.replace('profile ', '')] = {} - profiles[profile.replace('profile ', '')]['__name__'] = profile.replace('profile ', '') - for option in parser.options(profile): - profiles[profile.replace('profile ', '')][option] = parser.get(profile, option) - else: - safe_print('AWSume Error: Directory [' + file_path + '] does not exist') - return profiles - -def merge_role_and_source_profile(role_profile, source_profile): - """Merge the two profiles together to create a role/source profile combination. - The merged profile should be within the role_profile - - Parameters - ---------- - - role_profile - a role profile - - source_profile - the role_profile's source_profile - """ - LOG.info('merging config and credentials profile for [%s]', role_profile['__name__']) - LOG.debug('Role profile: %s', json.dumps(role_profile, indent=2)) - LOG.debug('Source profile: %s', json.dumps(source_profile, indent=2)) - if valid_profile(source_profile): - role_profile['aws_access_key_id'] = source_profile['aws_access_key_id'] - role_profile['aws_secret_access_key'] = source_profile['aws_secret_access_key'] - if 'aws_session_token' in source_profile: - role_profile['aws_session_token'] = source_profile['aws_session_token'] - if 'external_id' in source_profile: - role_profile['external_id'] = source_profile['external_id'] - if 'mfa_serial' not in role_profile and 'mfa_serial' in source_profile: - role_profile['mfa_serial'] = source_profile['mfa_serial'] - if 'region' not in role_profile and 'region' in source_profile: - role_profile['region'] = source_profile['region'] - -def mix_role_and_source_profiles(profiles): - """For any role profile in `profiles`, - add the aws_access_key_id and the aws_secret_access_key - from the source_profile to the role profile. - - Parameters - ---------- - - profiles - the collected aws profiles - - Returns - ------- - A dict of aws profiles with the roles combined with their source_profiles - """ - LOG.info('Combining role and source profiles') - for profile in profiles: - if is_role(profiles[profile]): - source_profile_name = profiles[profile]['source_profile'] - if profiles.get(source_profile_name): - merge_role_and_source_profile(profiles[profile], profiles[source_profile_name]) - else: - safe_print('AWSume profile configuration error: Source Profile [{}] for profile [{}] doesn\'t exist'.format(source_profile_name, profile)) - exit(0) - -def get_aws_profiles(app, args, config_file_path, credentials_file_path): - """Read the aws files and create dicts of the file data. - - Parameters - ---------- - - app - the AWSume app object - - args - the commandline arguments - - config_file_path - the path to the config file - - credentials_file_path - the path to the credentials file - - Returns - ------- - A dict of the aws files. - """ - LOG.info('Getting AWS profiles') - LOG.debug('Config path: %s', config_file_path) - LOG.debug('Credentials path: %s', credentials_file_path) - - config_profiles = read_ini_file(config_file_path) - credentials_profiles = read_ini_file(credentials_file_path) - trim_auto_profiles(credentials_profiles) - combined_profiles = {} - profile_names = list(config_profiles.keys()) + list(credentials_profiles.keys()) - - for profile in set(profile_names): - combined_profiles[profile] = {} - if profile in credentials_profiles: - combined_profiles[profile].update(credentials_profiles[profile]) - if profile in config_profiles: - combined_profiles[profile].update(config_profiles[profile]) - return combined_profiles - -def get_aws_profiles_callback(app, args, profiles): # pragma: no cover - """Execute what needs to be done right after the profiles are collected. - - Parameters - ---------- - - app - the AWSume app object - - args - the commandline arguments - - profiles - the collected aws profiles - """ - LOG.info('Validating Profile') - # list profiles - if args.list_profiles is True: - LOG.debug('Listing profile data') - list_profile_data(profiles) - exit(0) - try: - profile = profiles.get(args.target_profile_name) - if profile is None: - safe_print('AWSume error: Profile not found') - raise ProfileNotFoundError - else: - if not valid_profile(profile): - safe_print('AWSume error: Invalid profile') - raise InvalidProfileError - except ProfileNotFoundError: - LOG.debug('Profile not found') - if app.awsumeFunctions['catch_profile_not_found']: - for func in app.awsumeFunctions['catch_profile_not_found']: - func(app, args, profiles) - else: - exit(0) - except InvalidProfileError: - LOG.debug('Profile is invalid') - if app.awsumeFunctions['catch_invalid_profile']: - for func in app.awsumeFunctions['catch_invalid_profile']: - func(app, args, profiles, profile) - else: - exit(0) - -def trim_auto_profiles(profiles): - """Remove any profiles in the given `profiles` dict that are autoawsume profiles. - - Parameters - ---------- - - profiles - the collected aws profiles - """ - LOG.debug('Removing auto-refresh- profiles') - for profile in list(profiles): - if 'auto-refresh-' in profile: - profiles.pop(profile) - - - -# -# Listing Profiles -# -def get_account_id(profile): - """Return the account ID of the given profile if available. - - Parameters - ---------- - - profile - an aws profile - - Returns - ------- - A string containing the aws account ID of the given profile - if it is available, else return 'Unavailable'. - """ - LOG.info('Getting account ID from profile: %s', json.dumps(profile, indent=2)) - if profile.get('role_arn'): - return profile['role_arn'].replace('arn:aws:iam::', '').split(':')[0] - if profile.get('mfa_serial'): - return profile['mfa_serial'].replace('arn:aws:iam::', '').split(':')[0] - return 'Unavailable' - -def format_aws_profiles(profiles): - """Format the aws profiles for easy printing. - - Parameters - ---------- - - profiles - the collected aws profiles - - Returns - ------- - A well formatted list that makes it easy to print. - The first element in the list is a list of column headers. - The following elements in the list contain aws profile data, - one element per profile. - """ - LOG.info('Generating print-friendly profile data') - - sorted_profiles = collections.OrderedDict(sorted(profiles.items())) - - # List headers - list_headers = ['PROFILE', 'TYPE', 'SOURCE', 'MFA?', 'REGION', 'ACCOUNT'] - profile_list = [] - profile_list.append([]) - profile_list[0].extend(list_headers) - #now fill the tables with the appropriate data - for name in sorted_profiles: - #don't add any autoawsume profiles - if 'auto-refresh-' not in name: - profile = sorted_profiles[name] - is_role_profile = is_role(profile) - profile_type = 'Role' if is_role_profile else 'User' - source_profile = profile['source_profile'] if is_role_profile else 'None' - mfa_needed = 'Yes' if 'mfa_serial' in profile else 'No' - profile_region = str(profile.get('region')) - profile_account_id = get_account_id(profile) - list_row = [name, profile_type, source_profile, mfa_needed, profile_region, profile_account_id] - profile_list.append(list_row) - return profile_list - -def print_formatted_data(profile_data): # pragma: no cover - """Print the given profile data. - - Parameters - ---------- - - profile_data - the list of profile data that's returned from `format_aws_profiles` - """ - LOG.info('Printing formatted profile data') - print('Listing...\n') - - widths = [max(map(len, col)) for col in zip(*profile_data)] - print('AWS Profiles'.center(sum(widths) + 10, '=')) - for row in profile_data: - print(' '.join((val.ljust(width) for val, width in zip(row, widths)))) - -def list_profile_data(profiles): - """List useful information about the collected aws profiles. - - Parameters - ---------- - - profiles - the collected aws profiles - """ - LOG.info('Listing aws profiles') - - formatted_profiles = format_aws_profiles(profiles) - print_formatted_data(formatted_profiles) - -def get_profile_names(args, app): - """Get a list of all awsume-able profile names - - Parameters - ---------- - - args - the commandline args - - app - the AWSume app object - - Returns - ------- - A list of profile names - """ - LOG.info('Getting profile names') - profiles = {} - profiles = get_aws_profiles(app, args, AWS_CONFIG_FILE, AWS_CREDENTIALS_FILE) - mix_role_and_source_profiles(profiles) - profile_names = [] - for profile in profiles: - profile_names.append(profile) - return profile_names - -def list_profile_names(args, app): - """Handle listProfilenames argument flag. Print a list of profile names. - - Parameters - ---------- - - args - the commandline arguments - - app - the AWSume app object - """ - LOG.info('Listing profile names') - profile_names = [] - for func in app.awsumeFunctions['get_profile_names']: - profile_names.extend(func(args, app)) - print('\n'.join(profile_names)) - - - -# -# InspectionAndValidation -# -def valid_profile(profile): - """Checks to see if the given profile is valid. - A profile is valid if it is either: - - a non-role profile with both aws_access_key_id and aws_secret_access_key - - a valid role profile - - Parameters - ---------- - - profile - the profile to inspect - - Returns - ------- - True if the profile is valid, False if it isn't. - """ - LOG.debug('Checking profile validity: %s', json.dumps(profile, indent=2)) - if all(key in profile for key in ['aws_access_key_id', 'aws_secret_access_key']): - return True - if is_role(profile): - return True - LOG.debug('Invalid profile:\n%s', json.dumps(profile, default=str, indent=2)) - return False - -def requires_mfa(profile): - """Checks to see if the given profile requires MFA. - - Parameters - ---------- - - profile - the profile to inspect - - Returns - ------- - True if the profile requires MFA, False if it doesn't. - """ - return 'mfa_serial' in profile - -def is_role(profile): - """Checks to see if the given profile is a role profile. - A profile is a role profile if it contains a 'role_arn' and a 'source_profile'. - - Parameters - ---------- - - profile - the profile to inspect - - Returns - ------- - True if the profile is a role profile, False if it doesn't. - """ - if 'source_profile' in profile and 'role_arn' in profile: - return True - return False - -def valid_mfa_token(token): - """Checks to see if the given mfa token is a valid 6-digit mfa token. - - Parameters - ---------- - - token - the token to validate - - Returns - ------- - True if the given token is a valid mfa token, False if it isn't. - """ - LOG.debug('Validating MFA token: %s', token) - token_pattern = re.compile('^[0-9]{6}$') - if not token_pattern.match(token): - LOG.debug('%s is not a valid mfa token', token) - return False - return True - -def valid_cache_session(session): - """Determine if the given session is valid. - Check if it is expired. - - Parameters - ---------- - - session - the session to verify - - Returns - ------- - True if the session is valid, false if it isn't. - """ - LOG.info('Validating cache session') - LOG.debug(session) - try: - session_expiration = datetime.strptime(session['Expiration'], '%Y-%m-%d %H:%M:%S') - if session_expiration > datetime.now(): - return True - LOG.debug('Session is expired') - except Exception: - LOG.debug('Session is invalid') - return False - -def fix_session_credentials(session, profiles, args): - """Format the given session. - In particular fix the expiration to be of local timezone. - - Parameters - ---------- - - session - the session credentials from the get_session_token api call - - profiles - the collected aws profiles - - args - the commandline args - """ - LOG.debug('Converting session expiration to local timezone') - session['Expiration'] = session['Expiration'].astimezone(dateutil.tz.tzlocal()) - session['Expiration'] = session['Expiration'].strftime('%Y-%m-%d %H:%M:%S') - - region = profiles[args.target_profile_name].get('region') - if not region and profiles.get('default'): - LOG.debug('region not found in profile, using default profile\'s region') - region = profiles['default'].get('region') - session['region'] = region - -def get_duration(args, profile): - """Return the targeted duration. - If there is no targeted duration, return None. - - Parameters - ---------- - - args - the commandline args - - profile - the target aws profile - """ - if args.role_duration == 0: - return None - if args.role_duration: - return int(args.role_duration) - if profile.get('role_duration'): - return int(profile.get('role_duration')) - if AWSUME_OPTIONS.get('role-duration'): - return int(AWSUME_OPTIONS.get('role-duration')) - return None - - - -# -# Input/Output -# -def get_input(): # pragma: no cover - """A simple wrapper around the `read_input` python function. - - Returns - ------- - The value returned from `read_input()`. - """ - return read_input() - -def safe_print(text, end=None, color=Fore.RESET, style=Style.RESET_ALL): # pragma: no cover - """A simple wrapper around the builting `print` function. - It should always print to stderr to not interfere with the shell wrappers. - It should not use any colors or styles when running on Windows. - - Parameters - ---------- - - text - the text to print - - end - the character to use in the end - - color - the colorama color to use when printing - - style - the style to use when printing - """ - old_stderr = sys.stderr - sys.stderr = sys.__stderr__ - if not AWSUME_OPTIONS.get('colors') or os.name == 'nt': - print(text, file=sys.stderr, end=end) - else: - print(style + color + text + Style.RESET_ALL, file=sys.stderr, end=end) - sys.stderr = old_stderr - -def get_mfa(mfa_token_cli=None): - """Get the MFA token. - - This is either read from the CLI args or prompted interactively. - If the token is invalid, retry. - - Returns - ------- - The mfa token. - """ - if mfa_token_cli is not None: - safe_print('Using MFA token from cli args.', color=Fore.GREEN) - if valid_mfa_token(mfa_token_cli): - return mfa_token_cli - else: - safe_print('Given MFA code "{}" is invalid'.format(mfa_token_cli), color=Fore.RED) - - while True: - safe_print('Enter MFA token: ', '', Fore.LIGHTCYAN_EX) - mfa_token = get_input() - if valid_mfa_token(mfa_token): - return mfa_token - else: - safe_print('Please enter a valid MFA token: ', '', Fore.YELLOW) - -def config_help(app): - """Display the config help dialog. - - Parameters - ---------- - - app - the AWSume app object - """ - biggest_option = max([len(option) for option in app.valid_options]) - biggest_description = max([len(option[0]) for option in app.valid_options.values()]) - biggest_values = max([len(option[1]) for option in app.valid_options.values()]) - safe_print('OPTION'.ljust(biggest_option) + ' ' + 'DESCRIPTION'.ljust(biggest_description) + ' ' + 'VALUES'.ljust(biggest_values) + ' ' + 'CURRENT', None, Fore.BLUE) - for option in app.valid_options: - safe_print(option.ljust(biggest_option), ' ', Fore.GREEN) - safe_print(app.valid_options[option][0].ljust(biggest_description), ' ', Fore.GREEN) - safe_print(app.valid_options[option][1].ljust(biggest_values), ' ', Fore.GREEN) - safe_print(str(AWSUME_OPTIONS[option]), None, Fore.GREEN) - - - -# -# Caching sessions -# -def read_aws_cache(cache_path, cache_name): - """Read the aws cache file. - - Parameters - ---------- - - cache_path - the path to the aws cache directory - - cache_name - the name of the cache file - - Returns - ------- - The read credentials object if the file exists, {} if it doesn't. - """ - LOG.info('Reading aws cache file') - try: - if os.path.isfile(cache_path + cache_name): - LOG.debug('cache file exists, loading it') - session = json.load(open(cache_path + cache_name)) - return session - LOG.debug('cache file does not exist') - return {} - except Exception as exception: - LOG.debug('Exception when reading cache: %s', exception) - return {} - -def write_aws_cache(cache_path, cache_name, session): - """Write the session to a file. - - Parameters - ---------- - - cache_path - the path to the aws cache directory - - cache_name - the name of the cache file - - session - the session to write - """ - LOG.info('writing aws cache session') - LOG.debug('session to cache: %s', json.dumps(session, indent=2)) - if not os.path.exists(cache_path): - LOG.debug('cache directory does not exist, making it') - os.makedirs(cache_path) - json.dump(session, open(cache_path + cache_name, 'w'), indent=2, default=str) - - - -# -# AWSume workflow -# -def pre_awsume(app, args): - """Execute anything that needs to be handled before awsume. - Check for any specific flags and handle them accordingly. - Set the `target_profile_name`. If `profile_name` is none, target the default profile. - - Parameters - ---------- - - app - the AWSume app object - - args - the commandline arguments - """ - LOG.info('Preparing to run the AWSume workflow') - - if args.info: # pragma: no cover - LOG.setLevel(logging.INFO) - LOG.info('Info logs are visible') - if args.debug: # pragma: no cover - LOG.setLevel(logging.DEBUG) - LOG.debug('Debug logs are visible') - - if args.profile_name is None: - LOG.debug('Profilename not given, using default') - args.target_profile_name = 'default' - else: - LOG.debug('Using profilename: %s', args.profile_name) - args.target_profile_name = args.profile_name - - if args.version: # pragma: no cover - LOG.debug('version flag triggered') - safe_print(__version__) - exit(0) - - if args.config_help: - config_help(app) - exit(0) - - if args.config: - app.set_option(AWSUME_OPTIONS_FILE, args.config[0], args.config[1]) - exit(0) - - if args.unset_variables: - LOG.debug('unsetting environment variables') - app.set_export_data({'AWSUME_FLAG' : 'Unset', 'AWSUME_LIST' : []}) - app.export_data() - exit(0) - - if args.kill: - LOG.debug('kill flag triggered') - kill(args, app) - exit(0) - - if args.list_profile_names: - LOG.debug('Listing profile names') - list_profile_names(args, app) - exit(0) - - if args.plugin_urls: - LOG.debug('Installing plugin from %s', args.plugin_urls) - download_plugin(*args.plugin_urls) - exit(0) - - if args.delete_plugin_name: - LOG.debug('Attempting to delete plugin: %s', args.delete_plugin_name[0]) - delete_plugin(args.delete_plugin_name[0]) - exit(0) - - if args.display_plugin_info: - LOG.debug('displaying plugin info') - display_plugin_info(app.plugin_manager) - exit(0) - -def create_sts_client(aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None): - """Create a Boto3 STS client with the given credentials. - - Parameters - ---------- - - aws_access_key_id - the access key id that will be used to create the client - - aws_secret_access_key - the secret access key that will be used to create the client - - aws_session_token - the session token that will be used to create the client - - Returns - ------- - A Boto3 STS client. - """ - LOG.info('Creating an STS client') - sts_client = boto3.client('sts', - aws_access_key_id=aws_access_key_id, - aws_secret_access_key=aws_secret_access_key, - aws_session_token=aws_session_token) - return sts_client - -def get_user_session(app, args, profiles, cache_path, user_session): - """Call get-session-token to get the user session credentials. - If the profile is a user profile that doesn't require MFA (just an aws_access_key_id and - an aws_secret_access_key), then return the credentials without a session token. - - Parameters - ---------- - - app - the AWSume app object - - args - the command-line args - - profiles - the collected aws profiles - - cache_path - the directory to the cache file - - user_session - the state of the previously called get_user_session - - Returns - ------- - The session credentials from the get-session-token api call. - """ - LOG.info('Getting user session credentials') - profile = profiles[args.target_profile_name] - - - if profile.get('aws_session_token'): - LOG.debug('Profile already has session token, using it') - credentials = { - 'AccessKeyId' : profile.get('aws_access_key_id'), - 'SecretAccessKey' : profile.get('aws_secret_access_key'), - 'SessionToken' : profile.get('aws_session_token'), - 'region' : profile.get('region') - } - return credentials - - if not is_role(profile) and not requires_mfa(profile): - LOG.debug('Profile is a user that does not require MFA') - credentials = { - 'AccessKeyId' : profile.get('aws_access_key_id'), - 'SecretAccessKey' : profile.get('aws_secret_access_key'), - 'region' : profile.get('region') - } - return credentials - - cache_file_name = 'awsume-credentials-' - cache_file_name += args.target_profile_name if not is_role(profile) else profile['source_profile'] - cache_session = read_aws_cache(cache_path, cache_file_name) - if args.force_refresh is False and valid_cache_session(cache_session): - LOG.debug('returning cache session: %s', json.dumps(cache_session, indent=2)) - return cache_session - - sts_client = create_sts_client(profile['aws_access_key_id'], profile['aws_secret_access_key']) - try: - if requires_mfa(profile): - LOG.debug('profile requires mfa') - mfa_token = get_mfa(args.mfa_token) - response = sts_client.get_session_token(SerialNumber=profile['mfa_serial'], - TokenCode=mfa_token) - fix_session_credentials(response['Credentials'], profiles, args) - LOG.debug(response['Credentials']) - write_aws_cache(cache_path, cache_file_name, response['Credentials']) - return response['Credentials'] - else: - LOG.debug('profile does not require mfa') - response = sts_client.get_session_token() - fix_session_credentials(response['Credentials'], profiles, args) - LOG.debug(response['Credentials']) - return response['Credentials'] - except botocore.exceptions.ClientError as exception: - safe_print('AWSume error: ' + exception.response['Error']['Message'], None, Fore.RED) - raise UserAuthenticationError - except botocore.exceptions.ParamValidationError as exception: - safe_print('AWSume error: ' + str(exception), None, Fore.RED) - raise UserAuthenticationError - - - -def get_role_session(app, args, profiles, user_session, role_session): - """Call assume-role to get the role session credentials. - - Parameters - ---------- - - app - the AWSume app object - - args - the command-line arguments - - profiles - the collected aws profiles - - user_session - the user session credentials - - role_session - the state of the previously called get_role_session - - Returns - ------- - The session credentials from the assume-role api call - """ - LOG.info('Getting role session credentials') - - cache_file_name = 'awsume-role-credentials-' - cache_file_name += args.target_profile_name - cache_session = read_aws_cache(AWS_CACHE_DIRECTORY, cache_file_name) - if args.force_refresh is False and valid_cache_session(cache_session): - LOG.debug('returning cache session: %s', json.dumps(cache_session, indent=2)) - return cache_session - - profile = profiles[args.target_profile_name] - if args.session_name: - LOG.debug('using custom session name: %s', args.session_name) - role_session_name = args.session_name - else: - role_session_name = 'awsume-session-' + args.target_profile_name - sts_client = create_sts_client(user_session['AccessKeyId'], - user_session['SecretAccessKey'], - user_session.get('SessionToken')) - - optional_args = {"ExternalId": profile.get('external_id')} - other_available_args = {k: v for k, v in optional_args.items() if v != None} # remove any args that aren't set - - try: - if args.target_role_duration: - if requires_mfa(profile): - response = sts_client.assume_role(RoleArn=profile['role_arn'], - RoleSessionName=role_session_name, - DurationSeconds=args.target_role_duration, - SerialNumber=profile.get('mfa_serial'), - TokenCode=get_mfa(args.mfa_token), - **other_available_args) - else: - response = sts_client.assume_role(RoleArn=profile['role_arn'], - RoleSessionName=role_session_name, - DurationSeconds=args.target_role_duration, - **other_available_args) - fix_session_credentials(response['Credentials'], profiles, args) - write_aws_cache(AWS_CACHE_DIRECTORY, cache_file_name, response['Credentials']) - else: - response = sts_client.assume_role(RoleArn=profile['role_arn'], - RoleSessionName=role_session_name, - **other_available_args) - fix_session_credentials(response['Credentials'], profiles, args) - LOG.debug(response['Credentials']) - return response['Credentials'] - except botocore.exceptions.ClientError as exception: - safe_print('AWSume error: ' + exception.response['Error']['Message'], None, Fore.RED) - raise RoleAuthenticationError - except botocore.exceptions.ParamValidationError as exception: - safe_print('AWSume error: ' + str(exception), None, Fore.RED) - raise RoleAuthenticationError - -def get_role_session_callback(app, args, profiles, user_session, role_session): # pragma: no cover - """Call assume-role to get the role session credentials. - - Parameters - ---------- - - app - the AWSume app object - - args - the command-line args - - profiles - the collected aws profiles - - user_session - the user session credentials - - role_session - the state of the previously called get_role_session - """ - if args.auto_refresh: - LOG.debug('starting auto refresher') - start_auto_awsume(args, app, profiles, AWS_CREDENTIALS_FILE, user_session, role_session) - - - -# -# AutoAwsume -# -def start_auto_awsume(args, app, profiles, credentials_file_path, user_session, role_session): - """Start autoawsume. - - Parameters - ---------- - - args - the commandline args - - app - the AWSume app object - - profiles - the collected aws profiles - - credentials_file_path - the path to the credentials file - - user_session - the session credentials from the get-session-token api call - - role_session - the session credentials from the assume-role api call - """ - LOG.info('starting auto refresher') - profile = profiles[args.target_profile_name] - if args.session_name: - role_session_name = args.session_name - LOG.debug('custom session name: %s', role_session_name) - else: - role_session_name = 'awsume-session-' + args.target_profile_name - LOG.debug('default session name: %s', role_session_name) - auto_profile = create_auto_profile(role_session, - user_session, - role_session_name, - profile['source_profile'], - profile['role_arn']) - write_auto_awsume_session(args.target_profile_name, auto_profile, credentials_file_path) - kill_all_auto_processes() - data_list = [ - str('auto-refresh-' + args.target_profile_name), - str(role_session['region']), - str(args.target_profile_name) - ] - data = { - 'AWSUME_FLAG' : 'Auto', - 'AWSUME_LIST' : data_list - } - app.set_export_data(data) - -def is_auto_profiles(credentials_file_path=AWS_CREDENTIALS_FILE): - """Return whether or not there are auto-refresh- profiles in the credentials file. - - Parameters - ---------- - - credentials_file_path - the path to the aws credentials file - - Returns - ------- - True if there are auto-refresh- profiles, False if there aren't - """ - LOG.info('checking for auto-refresh- profiles in the credentials file') - auto_awsume_parser = ConfigParser.ConfigParser() - auto_awsume_parser.read(credentials_file_path) - for profile in auto_awsume_parser.sections(): - if 'auto-refresh-' in profile: - return True - return False - -def remove_auto_profile(profile_name=None): - """Remove the given profile from the credentials file. - Prefix `profile_name` with 'auto-refresh-' so that we wont delete non-autoawsume profiles. - If `profile_name` is none, remove all auto profiles. - - Parameters - ---------- - - profile - the profile that must be removed from the credentials file - """ - auto_awsume_parser = ConfigParser.ConfigParser() - auto_awsume_parser.read(AWS_CREDENTIALS_FILE) - if profile_name: - LOG.debug('removing auto-refresh- profile: %s', profile_name) - auto_profile_name = 'auto-refresh-' + profile_name - if auto_awsume_parser.has_section(auto_profile_name): - auto_awsume_parser.remove_section(auto_profile_name) - else: - LOG.debug('removing all auto-refresh- profiles') - for profile in auto_awsume_parser.sections(): - if 'auto-refresh-' in profile: - LOG.debug('removing auto-refresh- profile: %s', profile) - auto_awsume_parser.remove_section(profile) - auto_awsume_parser.write(open(AWS_CREDENTIALS_FILE, 'w')) - -def write_auto_awsume_session(profile_name, auto_profile, credentials_file_path): - """Write the auto-refresh- profile to the credentials file. - - Parameters - ---------- - - auto_profile - the profile to be written - - credentials_file_path - the path to the credentials file - """ - LOG.info('Writing auto-awsume session') - LOG.debug('Profile name: %s', profile_name) - LOG.debug('AutoAwsume profile: %s', json.dumps(auto_profile, indent=2)) - auto_profile_name = 'auto-refresh-' + profile_name - auto_awsume_parser = ConfigParser.ConfigParser() - auto_awsume_parser.read(credentials_file_path) - if auto_awsume_parser.has_section(auto_profile_name): - auto_awsume_parser.remove_section(auto_profile_name) - auto_awsume_parser.add_section(auto_profile_name) - for key in auto_profile: - auto_awsume_parser.set(auto_profile_name, key, str(auto_profile.get(key))) - auto_awsume_parser.write(open(credentials_file_path, 'w')) - -def create_auto_profile(role_session, user_session, session_name, source_profile_name, role_arn): - """Create the profile that'll be stored in the credentials file for autoawsume. - - Parameters - ---------- - - role_session - the session credentials from the assume-role api call - - user_session - the session credentials from the get-session-token api call - - session_name - the name to give to the role session - - source_profile_name - the name of the source profile - - Returns - ------- - The autoawsume profile - """ - return { - 'aws_access_key_id' : role_session['AccessKeyId'], - 'aws_secret_access_key' : role_session['SecretAccessKey'], - 'aws_session_token' : role_session['SessionToken'], - 'aws_region' : role_session['region'], - 'awsume_role_expiration' : role_session['Expiration'], - 'awsume_user_expiration' : user_session['Expiration'], - 'awsume_session_name' : session_name, - 'awsume_cache_name' : 'awsume-credentials-' + source_profile_name, - 'aws_role_arn' : role_arn, - } - -def kill_all_auto_processes(): - """Kill all running autoawsume processes.""" - LOG.info('Killing all autoawsume processes') - - for proc in psutil.process_iter(): - try: - for command_string in proc.cmdline(): - if 'autoawsume' in command_string: - LOG.debug('Found an autoawsume process, killing it') - proc.kill() - except Exception: - pass - -def kill(args, app): - """Handle the kill flag. - - Parameters - ---------- - - args - the command-line arguments - - app - the AWSume app object - """ - if args.profile_name: - LOG.debug('Will no longer auto refresh profile: %s', args.profile_name) - remove_auto_profile(args.profile_name) - if not is_auto_profiles(AWS_CREDENTIALS_FILE): - LOG.debug('No profiles left to refresh, ') - kill_all_auto_processes() - else: - app.set_export_data({'AWSUME_FLAG' : 'Stop', 'AWSUME_LIST': [args.profile_name]}) - app.export_data() - return - else: - LOG.debug('Killing auto-refresher, no longer refreshing any profiles.') - kill_all_auto_processes() - remove_auto_profile() - app.set_export_data({'AWSUME_FLAG' : 'Kill', 'AWSUME_LIST' : []}) - app.export_data() - - - -# -# Plugin Management -# -def get_main_content_type(url_info): - """Return the main content type of an HTTPMessage object in a Python 2 and 3 compatible way. - - Parameters - ---------- - - url_info - the HTTPMessage object - - Returns - ------- - a string containing the main content type - """ - try: # Python 3 - return url_info.get_content_maintype() - except AttributeError: # Python 2 - return url_info.getmaintype() - -def download_file(url): - """Download a file from the given url and return a string of it's contents - - Parameters - ---------- - - url - the url to the file to download - - Returns - ------- - a string of the file contents - """ - response = six.moves.urllib.request.urlopen(url) - content_type = get_main_content_type(response.info()) - if content_type != 'text' and content_type != 'binary': - safe_print('AWSume error: The file needs to be a plain text file, received [' + str(content_type) + ']', None, Fore.RED) - raise Exception - download = response.read() - return download.decode('utf-8') - -def write_plugin_files(file1, file2, filename1, filename2): - """Write the given files to the plugin directory. - - Parameters - ---------- - - file1 - the contents of the first plugin file - - file2 - the contents of the second plugin file - - filename1 - the name of the first plugin file - - filename2 - the name of the second plugin file - """ - filepath1 = os.path.join(AWSUME_PLUGIN_DIRECTORY, filename1) - filepath2 = os.path.join(AWSUME_PLUGIN_DIRECTORY, filename2) - - if os.path.isfile(filepath1) or os.path.isfile(filepath2): - safe_print('It looks like that plugin is already installed, would you like to overwrite it? (y/N) : ', '', Fore.YELLOW) - choice = get_input() - if not choice.startswith('y') and not choice.startswith('Y'): - return - safe_print('Saving ' + filename1 + ' and ' + filename2 + ' to ' + AWSUME_PLUGIN_DIRECTORY, None, Fore.GREEN) - with open(filepath1, 'w') as writefile: - writefile.write(file1) - with open(filepath2, 'w') as writefile: - writefile.write(file2) - -def download_plugin(url1, url2): - """Download the plugin from the given url. There should be two downloads: one .py file and one .yapsy-plugin file. - - Parameters - ---------- - - url1 - ideally the url to the .py file, but could be switched - - url2 - ideally the url to the .yapsy-plugin file, but could be switched - """ - LOG.info('Downloading plugins') - url1 = url1.replace('\\', '/') - url2 = url2.replace('\\', '/') - filename1 = url1.split('?')[0].split('/')[-1] - filename2 = url2.split('?')[0].split('/')[-1] - - if not filename1 or not filename2: - safe_print('AWSume error: Please provide a url to a valid file.', None, Fore.RED) - return - - if not (filename1.endswith('.py') and filename2.endswith('.yapsy-plugin') or - filename1.endswith('.yapsy-plugin') and filename2.endswith('.py')): - safe_print('AWSume error: Please supply urls to one .py file and one .yapsy-plugin file', None, Fore.RED) - return - if filename1 == url1 and filename2 == url2: - cache = read_plugin_cache() - if cache.get(filename1) and cache.get(filename2): - url1 = cache[filename1] - url2 = cache[filename2] - try: - LOG.debug('Downloading from %s', url1) - file1 = download_file(url1) - LOG.debug('Downloading from %s', url2) - file2 = download_file(url2) - except Exception as exception: - safe_print('AWSume error: Could not download files: ' + str(exception), None, Fore.RED) - return - - write_plugin_files(file1, file2, filename1, filename2) - cache_urls(url1, url2, filename1, filename2) - -def delete_plugin(plugin_name): - """Delete the .py and .yapsy-plugin file given by `plugin_name` from the plugins directory. - - Parameters - ---------- - - plugin_name - the name of the plugin to delete - """ - - directory = os.listdir(AWSUME_PLUGIN_DIRECTORY) - plugins = [item for item in directory if item.endswith('.yapsy-plugin')] - plugins = [name.split('.yapsy-plugin')[0] for name in plugins] - if plugin_name not in plugins: - safe_print('That plugin doesn\'t exist', None, Fore.YELLOW) - return - - plugin_files = [item for item in directory if plugin_name in item] - safe_print('All plugin files will be deleted, are you sure you want to delete the plugin: [' + plugin_name + ']', None, Fore.YELLOW) - safe_print('\n'.join(plugin_files), None, Fore.YELLOW) - safe_print('(y/N)? ', '', Fore.YELLOW) - choice = get_input() - if not choice.startswith('y') and not choice.startswith('Y'): - return - for item in plugin_files: - item_path = os.path.join(AWSUME_PLUGIN_DIRECTORY, item) - if os.path.isfile(item_path): - safe_print('Deleting file: ' + item, None, Fore.YELLOW) - os.remove(item_path) - elif os.path.isdir(item_path): - safe_print('Deleting directory and contents: ' + item, None, Fore.YELLOW) - shutil.rmtree(item_path) - -def read_plugin_cache(): - """Read the plugin cache. - - Returns - ------- - The cache'd object. - """ - if os.path.isfile(AWSUME_PLUGIN_CACHE_FILE): - try: - return json.load(open(AWSUME_PLUGIN_CACHE_FILE, 'r')) - except Exception: - json.dump({}, open(AWSUME_PLUGIN_CACHE_FILE, 'w')) - return {} - -def cache_urls(url1, url2, filename1, filename2): - """Cache the plugin urls for the given `plugin_name` - - Parameters - ---------- - - url1 - one of the urls to the plugin - - url2 - one of the urls to the plugin - - filename1 - the name to remember url1 as - - filename2 - the name to remember url2 as - """ - cache = read_plugin_cache() - cache[filename1] = url1 - cache[filename2] = url2 - json.dump(cache, open(AWSUME_PLUGIN_CACHE_FILE, 'w'), indent=2) - -def display_plugin_info(manager): - """Display useful information about installed plugins - - Parameters - ---------- - - manager - the plugin manager - """ - cache = read_plugin_cache() - if cache: - safe_print('') - safe_print('===== Cached Plugins =====', None, Fore.BLUE) - for filename in cache: - safe_print(filename + ' ->', None, Fore.GREEN) - safe_print(' ' + cache[filename], None, Fore.YELLOW) - safe_print('===== Cached Plugins =====', None, Fore.BLUE) - - plugins = manager.getAllPlugins() - if plugins: - for plugin in manager.getAllPlugins(): - safe_print('') - safe_print('Name: ' + plugin.name, None, Fore.BLUE) - safe_print('Author: ' + plugin.author, None, Fore.GREEN) - safe_print('Version: ' + str(plugin.version), None, Fore.GREEN) - safe_print('Website: ' + plugin.website, None, Fore.GREEN) - safe_print('Description: ' + plugin.description, None, Fore.GREEN) - else: - safe_print('AWSume: You do not have any installed plugins.', None, Fore.YELLOW) - - - -# -# AWSume App -# -def create_plugin_manager(plugin_directory): - """Create the plugin manager, set the location to look for the plugins, and collect them.""" - plugin_manager = PluginManager.PluginManager() - plugin_manager.setPluginPlaces([plugin_directory]) - - # hide any output from stderr while loading the plugins - sys.stderr = open(os.devnull, 'w') - plugin_manager.locatePlugins() - processed_plugins = plugin_manager.loadPlugins() - sys.stderr = sys.__stderr__ - - # check for any errors while loading the plugins - for plugin_info in processed_plugins: - if plugin_info.error: - try: # raise the error so we can properly catch specific errors for custom feedback - __, err2, __ = plugin_info.error - safe_print('Unable to load plugin [' + plugin_info.name + ']: ' + str(err2)) - raise err2 - except ImportError as error: - if 'No module named ' in str(error): - module_name = str(error).replace('No module named ', '').replace("'",'') - safe_print(' try running "pip install ' + module_name + '"') - except Exception: - pass - - return plugin_manager - -def register_plugins(app, manager): - """Register all available plugins from the manager. - - Parameters - ---------- - - app - the AWSume app object - - manager - a yapsy plugin manager - """ - for plugin in manager.getAllPlugins(): - try: - if plugin.plugin_object.TARGET_VERSION.split('.')[0] != __version__.split('.')[0]: - safe_print('AWSume warning: [{}] targets AWSume version {}'.format(plugin.name, plugin.plugin_object.TARGET_VERSION), None, Fore.YELLOW) - except AttributeError: - safe_print('AWSume warning: [{}] has no targeted version. AWSume may not work as expected.'.format(plugin.name), None, Fore.YELLOW) - - for function_type in app.validFunctions: - if function_type in dir(plugin.plugin_object): - if not app.register_function(function_type, getattr(plugin.plugin_object, function_type)): - safe_print('Unable to register plugin [{}] function of type {}'.format(plugin.name, function_type), None, Fore.YELLOW) - -def awsume(app, args, profiles): - """The normal workflow for awsume. - Call get-session-token and assume-role. - - Parameters - ---------- - - app - the AWSume app object - - args - the command-line args - - profiles - the collected aws profiles - - user_session - the user session credentials - - role_session - the state of the previously called get_role_session - - Returns - ------- - Session credentials - """ - user_session = None - try: - for func in app.awsumeFunctions['get_user_session']: - user_session = func(app, args, profiles, AWS_CACHE_DIRECTORY, user_session) - LOG.debug('User session:\n%s', json.dumps(user_session, default=str, indent=2)) - if user_session.get('Expiration'): - safe_print('AWSume: User profile credentials will expire at: ' + str(user_session['Expiration']), None, Fore.GREEN) - for func in app.awsumeFunctions['get_user_session_callback']: - func(app, args, profiles, user_session) - except UserAuthenticationError: - LOG.debug('UserAuthenticationError raised') - if app.awsumeFunctions['catch_user_authentication_error']: - for func in app.awsumeFunctions['catch_user_authentication_error']: - func(app, args, profiles) - else: - exit(0) - - role_session = None - try: - if is_role(profiles[args.target_profile_name]): - for func in app.awsumeFunctions['get_role_session']: - role_session = func(app, args, profiles, user_session, role_session) - LOG.debug('Role session:\n%s', json.dumps(role_session, default=str, indent=2)) - safe_print('AWSume: Role profile credentials will expire at: ' + str(role_session['Expiration']), None, Fore.GREEN) - for func in app.awsumeFunctions['get_role_session_callback']: - func(app, args, profiles, user_session, role_session) - except RoleAuthenticationError: - LOG.debug('RoleAuthenticationError raised') - if app.awsumeFunctions['catch_role_authentication_error']: - for func in app.awsumeFunctions['catch_role_authentication_error']: - func(app, args, profiles, user_session) - else: - exit(0) - return user_session, role_session - -def awsume_role_duration(app, args, profiles): - """The normal workflow for awsume. - Call get-session-token and assume-role. - - Parameters - ---------- - - app - the AWSume app object - - args - the command-line args - - profiles - the collected aws profiles - - user_session - the user session credentials - - role_session - the state of the previously called get_role_session - - Returns - ------- - Session credentials - """ - LOG.debug('Using a custom duration') - profile = profiles[args.target_profile_name] - user_session = { - 'AccessKeyId' : profile.get('aws_access_key_id'), - 'SecretAccessKey' : profile.get('aws_secret_access_key'), - 'region' : profile.get('region') - } - role_session = None - try: - for func in app.awsumeFunctions['get_role_session']: - role_session = func(app, args, profiles, user_session, role_session) - LOG.debug('Role session:\n%s', json.dumps(role_session, default=str, indent=2)) - safe_print('AWSume: Role profile credentials will expire at: ' + str(role_session['Expiration']), None, Fore.GREEN) - for func in app.awsumeFunctions['get_role_session_callback']: - func(app, args, profiles, user_session, role_session) - role_session = role_session - except RoleAuthenticationError: - safe_print('Calling awsume with custom role duration failed, calling awsume without custom duration...', None, Fore.YELLOW) - args.target_role_duration = 0 - user_session, role_session = awsume(app, args, profiles) - return user_session, role_session - -class AwsumeApp(object): - """The app that runs AWSume.""" - awsumeFunctions = {} - validFunctions = [ - 'add_arguments', - 'pre_awsume', - 'get_aws_profiles', - 'get_aws_profiles_callback', - 'get_user_session', - 'get_user_session_callback', - 'get_role_session', - 'get_role_session_callback', - 'post_awsume', - 'catch_profile_not_found', - 'catch_invalid_profile', - 'catch_user_authentication_error', - 'catch_role_authentication_error', - 'get_profile_names', - ] - __out_data = { - 'AWSUME_FLAG':'', - 'AWSUME_LIST':[], - 'exported':False, - } - options = {} - valid_options = { - 'colors': ['enable colored output', 'true or false'], - 'role-duration': ['custom role duration', '0 (off) to 43200 (12 hours)'], - } - - def __init__(self, plugin_manager): # pragma: no cover - """Create plugin function types, add defaults to the lists. - Load the awsume options. - - Parameters - ---------- - - plugin_manager - the main plugin manager - """ - for directory in [AWS_DIRECTORY, AWSUME_PLUGIN_DIRECTORY]: - if not os.path.exists(directory): - os.makedirs(directory) - for filename in [AWS_CREDENTIALS_FILE, AWS_CONFIG_FILE]: - if not os.path.isfile(filename): - open(filename, 'a').close() - self.load_options(AWSUME_OPTIONS_FILE) - self.plugin_manager = plugin_manager - for function_type in self.validFunctions: - self.awsumeFunctions[function_type] = [] - if globals().get(function_type): - self.register_function(function_type, globals()[function_type]) - - def load_options(self, options_path): - """Load awsume's options into the app.options object. - - Parameters - ---------- - - options_path - the path to the awsume options file - """ - try: - self.options = json.load(open(options_path, 'r')) - except Exception: - default_options = { - 'colors': True, - 'role-duration': 0, - } - json.dump(default_options, open(options_path, 'w'), indent=2) - self.options = default_options - global AWSUME_OPTIONS - AWSUME_OPTIONS = self.options - - def set_option(self, options_path, option_name, option_value): - """Set the value of an option to the given value. - Make sure the given option is valid. - Save the options to the options file. - - Parameters - ---------- - - options_path - the path to the awsume options file - - option_name - the name of the option to set - - option_value - the value to set that option to - """ - if option_name == 'colors': - if os.name == 'nt': - self.options[option_name] = False - safe_print('AWSume does not support colored output on Windows.') - elif option_value in ['yes', 'no', 'true', 'false', 't', 'f', '1', '0']: - self.options[option_name] = option_value.lower() in ('yes', 'true', 't', '1') - if self.options[option_name]: - safe_print('Colored output enabled!', None, Fore.GREEN) - else: - safe_print('Colored output disabled!', None, Fore.GREEN) - else: - safe_print('Colors option must be true or false!', None, Fore.RED) - elif option_name == 'role-duration': - if option_value.isdigit() and 0 <= int(option_value) and int(option_value) <= 43200: - self.options[option_name] = int(option_value) - if self.options[option_name]: - safe_print('Role duration set to: ' + str(self.options[option_name]), None, Fore.GREEN) - else: - safe_print('Role duration disabled', None, Fore.GREEN) - else: - safe_print('Role duration option must be an integer between 0 and 43200!', None, Fore.RED) - json.dump(self.options, open(options_path, 'w'), indent=2) - - def register_function(self, function_type, new_function): - """Register functions to the AWSume App. - - Returns - ------- - True if the function was successfully registered, False if it wasn't. - """ - if function_type in self.validFunctions: - self.awsumeFunctions[function_type].append(new_function) - return True - return False - - def set_export_data(self, data): - """Set the data that will be exported to the shell wrappers. - If data has already been exported, ignore any future data. - - Parameters - ---------- - - data - the data to set to be exported, should be a dict with keys: - - AWSUME_FLAG - a string that tells the shell wrapper what to do - - AWSUME_LIST - the data that needs to be sent to the shell wrapper - """ - if not self.__out_data['exported']: - LOG.debug('Data to be sent to the shell wrapper:\n%s', json.dumps(data, default=str, indent=2)) - self.__out_data['AWSUME_FLAG'] = data.get('AWSUME_FLAG') - self.__out_data['AWSUME_LIST'] = data.get('AWSUME_LIST') - self.__out_data['exported'] = True - - def export_data(self): - """Print the data, sending the session to the shell wrapper.""" - LOG.debug('Exporting data to shell wrapper') - print(str(self.__out_data['AWSUME_FLAG']), end=' ') - print(' '.join(self.__out_data['AWSUME_LIST'])) - - def run(self, system_arguments): - """Execute AWSume.""" - argument_parser = generate_argument_parser() - for func in self.awsumeFunctions['add_arguments']: - func(argument_parser) - arguments = parse_args(argument_parser, system_arguments) - - for func in self.awsumeFunctions['pre_awsume']: - func(self, arguments) - - # collect the aws profiles - profiles = {} - for func in self.awsumeFunctions['get_aws_profiles']: - new_profiles = func(self, arguments, AWS_CONFIG_FILE, AWS_CREDENTIALS_FILE) - profiles.update(new_profiles) - mix_role_and_source_profiles(profiles) - LOG.debug('Collected aws profiles:\n%s', json.dumps(profiles, default=str, indent=2)) - for func in self.awsumeFunctions['get_aws_profiles_callback']: - func(self, arguments, profiles) - - # decide which awsume workflow to take - arguments.target_role_duration = get_duration(arguments, profiles[arguments.target_profile_name]) - if arguments.target_role_duration and is_role(profiles[arguments.target_profile_name]): - user_session, role_session = awsume_role_duration(self, arguments, profiles) - else: - user_session, role_session = awsume(self, arguments, profiles) - session_to_use = role_session if role_session else user_session - - # export the credentials - data_list = [ - str(session_to_use.get('AccessKeyId')), - str(session_to_use.get('SecretAccessKey')), - str(session_to_use.get('SessionToken')), - str(session_to_use.get('region')), - str(arguments.target_profile_name) - ] - data = { - 'AWSUME_FLAG' : 'Awsume', - 'AWSUME_LIST' : data_list - } - self.set_export_data(data) - - for func in self.awsumeFunctions['post_awsume']: - func(self, arguments, profiles, user_session, role_session) - -def main(command_line_arguments=sys.argv[1:]): - """Create the AWSume app and plugin manager, then execute AWSume""" - plugin_manager = create_plugin_manager(AWSUME_PLUGIN_DIRECTORY) - awsume = AwsumeApp(plugin_manager) - if plugin_manager: - register_plugins(awsume, plugin_manager) - awsume.run(command_line_arguments) - awsume.export_data() - -if __name__ == '__main__': - main() diff --git a/awsume/awsumepy/__init__.py b/awsume/awsumepy/__init__.py new file mode 100644 index 0000000..2600e3d --- /dev/null +++ b/awsume/awsumepy/__init__.py @@ -0,0 +1,3 @@ +from . import * +from . hookimpl import hookimpl +from . lib import safe_print diff --git a/awsume/awsumepy/app.py b/awsume/awsumepy/app.py new file mode 100644 index 0000000..6e76f76 --- /dev/null +++ b/awsume/awsumepy/app.py @@ -0,0 +1,168 @@ +import os +import sys +import argparse +import json +import logging +import pluggy +import colorama +from colorama import init, deinit +from pathlib import Path + +from . lib.profile import aggregate_profiles +from . lib.config_management import load_config +from . lib.aws_files import get_aws_files +from . lib.exceptions import ProfileNotFoundError, InvalidProfileError, UserAuthenticationError, RoleAuthenticationError +from . lib.logger import logger +from . lib.safe_print import safe_print +from . lib import constants +from . import hookspec +from . import default_plugins + + +class Awsume(object): + def __init__(self): + self.plugin_manager = self.get_plugin_manager() + self.config = load_config() + init(autoreset=True) + if not self.config.get('colors') == 'true': + deinit() + + + def get_plugin_manager(self) -> pluggy.PluginManager: + logger.debug('Creating plugin manager') + pm = pluggy.PluginManager('awsume') + pm.add_hookspecs(hookspec) + logger.debug('Loading plugins') + pm.register(default_plugins) + pm.load_setuptools_entrypoints('awsume') + return pm + + + def parse_args(self, system_arguments: list) -> argparse.Namespace: + logger.debug('Gathering arguments') + epilog = """Thank you for using AWSume! Check us out at https://trek10.com""" + description="""Awsume - A cli that makes using AWS IAM credentials easy""" + argument_parser = argparse.ArgumentParser( + prog='awsume', + description=description, + epilog=epilog, + formatter_class=lambda prog: (argparse.RawDescriptionHelpFormatter(prog, max_help_position=80, width=80)), + ) + self.plugin_manager.hook.add_arguments( + config=self.config, + parser=argument_parser, + ) + logger.debug('Parsing arguments') + args = argument_parser.parse_args(system_arguments[1:]) + logger.debug('Handling arguments') + if args.refresh_autocomplete: + autocomplete_file = Path('~/.awsume/autocomplete.json').expanduser() + result = self.plugin_manager.hook.get_profile_names( + config=self.config, + arguments=args, + ) + profile_names = [y for x in result for y in x] + json.dump({'profile-names': profile_names}, open(autocomplete_file, 'w')) + exit(0) + self.plugin_manager.hook.post_add_arguments( + config=self.config, + arguments=args, + parser=argument_parser, + ) + return args + + + def get_profiles(self, args: argparse.Namespace) -> dict: + logger.debug('Gathering profiles') + config_file, credentials_file = get_aws_files(args, self.config) + self.plugin_manager.hook.pre_collect_aws_profiles( + config=self.config, + arguments=args, + credentials_file=credentials_file, + config_file=config_file, + ) + aws_profiles_result = self.plugin_manager.hook.collect_aws_profiles( + config=self.config, + arguments=args, + credentials_file=credentials_file, + config_file=config_file, + ) + profiles = aggregate_profiles(aws_profiles_result) + self.plugin_manager.hook.post_collect_aws_profiles( + config=self.config, + arguments=args, + profiles=profiles, + ) + return profiles + + + def get_credentials(self, args: argparse.Namespace, profiles: dict) -> dict: + logger.debug('Getting credentials') + try: + result = self.plugin_manager.hook.assume_role(config=self.config, arguments=args, profiles=profiles) + except ProfileNotFoundError as e: + safe_print(colorama.Fore.RED + str(e)) + logger.debug('', exc_info=True) + self.plugin_manager.hook.catch_profile_not_found_exception(config=self.config, arguments=args, profiles=profiles) + exit(1) + except InvalidProfileError as e: + safe_print(colorama.Fore.RED + str(e)) + logger.debug('', exc_info=True) + self.plugin_manager.hook.catch_invalid_profile_error(config=self.config, arguments=args, profiles=profiles) + exit(1) + except UserAuthenticationError as e: + safe_print(colorama.Fore.RED + str(e)) + logger.debug('', exc_info=True) + self.plugin_manager.hook.catch_user_authentication_error(config=self.config, arguments=args, profiles=profiles) + exit(1) + except RoleAuthenticationError as e: + safe_print(colorama.Fore.RED + str(e)) + logger.debug('', exc_info=True) + self.plugin_manager.hook.catch_role_authentication_error(config=self.config, arguments=args, profiles=profiles) + exit(1) + return result + + + def export_data(self, awsume_flag: str, awsume_list: list): + logger.debug('Exporting data') + print(awsume_flag) + print(' '.join(awsume_list)) + + + def run(self, system_arguments: list): + args = self.parse_args(system_arguments) + args.system_arguments = system_arguments + profiles = self.get_profiles(args) + + self.plugin_manager.hook.pre_assume_role( + config=self.config, + arguments=args, + profiles=profiles, + ) + if args.with_saml: + credentials = {} + elif args.with_web_identity: + credentials = {} + else: + assume_role_result = self.get_credentials(args, profiles) + credentials = next(_ for _ in assume_role_result if _) + self.plugin_manager.hook.post_assume_role( + config=self.config, + arguments=args, + profiles=profiles, + credentials=credentials, + ) + if args.auto_refresh: + self.export_data('Auto', [ + 'autoawsume-{}'.format(args.target_profile_name), + credentials.get('Region'), + args.target_profile_name, + ]) + else: + self.export_data('Awsume', [ + str(credentials.get('AccessKeyId')), + str(credentials.get('SecretAccessKey')), + str(credentials.get('SessionToken')), + str(credentials.get('Region')), + str(args.target_profile_name), + ]) diff --git a/awsume/awsumepy/default_plugins.py b/awsume/awsumepy/default_plugins.py new file mode 100644 index 0000000..a9b4a9e --- /dev/null +++ b/awsume/awsumepy/default_plugins.py @@ -0,0 +1,378 @@ +import argparse +import configparser +import boto3 +import sys +import colorama + +try: + import botostubs +except: + pass + + +from . lib.exceptions import UserAuthenticationError, RoleAuthenticationError +from . hookimpl import hookimpl +from .. import __data__ +from ..autoawsume.process import kill +from . lib import aws as aws_lib +from . lib import aws_files as aws_files_lib +from . lib.logger import logger +from . lib.safe_print import safe_print +from . lib import config_management as config_lib +from . lib.exceptions import ProfileNotFoundError +from . lib import profile as profile_lib +from . lib import cache as cache_lib + + +def custom_duration_argument_type(string): + number = int(string) + if number >= 0 and number <= 43201: + return number + raise argparse.ArgumentTypeError('Custom Duration must be between 0 and 43200') + + +@hookimpl(tryfirst=True) +def add_arguments(config: dict, parser: argparse.ArgumentParser): + parser.add_argument('-v', '--version', + action='store_true', + dest='version', + help='Display the current version of awsume', + ) + parser.add_argument('profile_name', + nargs='?', + action='store', + metavar='profile_name', + help='The target profile name', + ) + parser.add_argument('-r', '--refresh', + action='store_true', + dest='force_refresh', + help='Force refresh credentials', + ) + parser.add_argument('-s', '--show-commands', + action='store_true', + dest='show_commands', + help='Show the commands to set the credentials', + ) + parser.add_argument('-u', '--unset', + action='store_true', + dest='unset_variables', + help='Unset your aws environment variables', + ) + parser.add_argument('-a', '--auto-refresh', + action='store_true', + dest='auto_refresh', + help='Auto refresh credentials', + ) + parser.add_argument('-k', '--kill-refresher', + action='store_true', + default=False, + dest='kill', + help='Kill autoawsume', + ) + parser.add_argument('-l', '--list-profiles', + nargs='?', + action='store', + default=None, + const='list', + choices=['more', 'list', None], + metavar='more', + dest='list_profiles', + help='List profiles, "more" for detail (slow)', + ) + parser.add_argument('--refresh-autocomplete', + action='store_true', + dest='refresh_autocomplete', + help='Refresh all plugin autocomplete profiles', + ) + + parser.add_argument('--role-arn', + action='store', + dest='role_arn', + metavar='role_arn', + help='Role ARN to assume', + ) + parser.add_argument('--source-profile', + action='store', + dest='source_profile', + metavar='source_profile', + help='source_profile to use (role-arn only)', + ) + parser.add_argument('--external-id', + action='store', + dest='external_id', + metavar='external_id', + help='External ID to pass to the assume_role', + ) + parser.add_argument('--mfa-token', + action='store', + dest='mfa_token', + metavar='mfa_token', + help='Your mfa token', + ) + parser.add_argument('--region', + action='store', + dest='region', + metavar='region', + help='The region you want to awsume into', + ) + parser.add_argument('--session-name', + action='store', + dest='session_name', + metavar='session_name', + help='Set a custom role session name', + ) + parser.add_argument('--role-duration', + action='store', + dest='role_duration', + type=custom_duration_argument_type, + metavar='role_duration', + help='Seconds to get role creds for', + ) + + assume_role_method = parser.add_mutually_exclusive_group() + assume_role_method.add_argument('--with-saml', + action='store_true', + dest='with_saml', + help='Use saml (requires plugin)', + ) + assume_role_method.add_argument('--with-web-identity', + action='store_true', + dest='with_web_identity', + help='Use web identity (requires plugin)', + ) + + parser.add_argument('--credentials-file', + action='store', + dest='credentials_file', + metavar='credentials_file', + help='Target a shared credentials file', + ) + parser.add_argument('--config-file', + action='store', + dest='config_file', + metavar='config_file', + help='Target a config file', + ) + + parser.add_argument('--config', + nargs='*', + dest='config', + action='store', + metavar='option', + help='Configure awsume', + ) + + parser.add_argument('--info', + action='store_true', + dest='info', + help='Print any info logs to stderr', + ) + parser.add_argument('--debug', + action='store_true', + dest='debug', + help='Print any debug logs to stderr', + ) + + +@hookimpl(tryfirst=True) +def post_add_arguments(config: dict, arguments: argparse.Namespace, parser: argparse.ArgumentParser): + if arguments.role_arn and arguments.auto_refresh: + safe_print(colorama.Fore.RED + 'Cannot use autoawsume with given role_arn') + exit(1) + if arguments.version: + logger.debug('Logging version') + safe_print(__data__.version) + exit(0) + if arguments.unset_variables: + logger.debug('Unsetting environment variables') + print('Unset', []) + exit(0) + if arguments.config: + config_lib.update_config(arguments.config) + exit(0) + if arguments.kill: + kill(arguments) + exit(0) + + if arguments.role_arn and not arguments.role_arn.startswith('arn:aws:iam::'): + parts = arguments.role_arn.split(':') + if len(parts) != 2: + parser.error('--role-arn must be a valid role arn or follow the format ":"') + if not parts[0].isnumeric() or len(parts[0]) is not 12: + parser.error('--role-arn account id must be valid numeric account id of length 12') + arguments.role_arn = 'arn:aws:iam::{}:role/{}'.format(parts[0], parts[1]) + + if not arguments.profile_name: + if arguments.role_arn: + arguments.target_profile_name = arguments.role_arn + else: + arguments.target_profile_name = 'default' + else: + arguments.target_profile_name = arguments.profile_name + + +@hookimpl(tryfirst=True) +def collect_aws_profiles(config: dict, arguments: argparse.Namespace, credentials_file: str, config_file: str): + credentials = configparser.ConfigParser() + credentials.read(credentials_file) + profiles = {k: dict(v) for k, v in credentials._sections.items()} + + config = configparser.ConfigParser() + config.read(config_file) + config_profiles = {k: dict(v) for k, v in config._sections.items()} + for profile_name, profile in config_profiles.items(): + short_name = profile_name.replace('profile ', '') + if short_name not in profiles: + profiles[short_name] = {} + profiles[short_name].update(profile) + return profiles + + +@hookimpl(tryfirst=True) +def post_collect_aws_profiles(config: dict, arguments: argparse.Namespace, profiles: dict): + if arguments.list_profiles: + logger.debug('Listing profiles') + profile_lib.list_profile_data(profiles, arguments.list_profiles == 'more') + exit(0) + + +@hookimpl(tryfirst=True) +def assume_role(config: dict, arguments: argparse.Namespace, profiles: dict) -> dict: + region = profile_lib.get_region(profiles, arguments) + + # Use role_arn from cli instead of role profile + if arguments.role_arn: + role_duration = arguments.role_duration or int(config.get('role-duration')) + logger.debug('Using role_arn from the CLI') + session_name = arguments.session_name or 'awsume-cli-role' + if not arguments.source_profile: + role_session = aws_lib.assume_role({}, arguments.role_arn, session_name, region, arguments.external_id, role_duration) + return role_session + else: + logger.debug('Using the source_profile from the cli, not using existing creds to call assume_role') + source_profile = profiles.get(arguments.source_profile) + if not source_profile: + raise ProfileNotFoundError(profile_name=arguments.source_profile) + source_credentials = profile_lib.profile_to_credentials(source_profile) + if role_duration and 'mfa_serial' in source_profile: + source_session = source_credentials + role_session = aws_lib.assume_role( + source_session, + arguments.role_arn, + session_name, + region=region, + external_id=arguments.external_id, + role_duration=role_duration, + mfa_serial=source_profile['mfa_serial'], + mfa_token=arguments.mfa_token, + ) + else: + if 'mfa_serial' in source_profile: + source_session = aws_lib.get_session_token( + source_credentials, + region=profile_lib.get_region(profiles, arguments), + mfa_serial=source_profile.get('mfa_serial'), + mfa_token=arguments.mfa_token, + ignore_cache=arguments.force_refresh, + ) + else: + source_session = source_credentials + role_session = aws_lib.assume_role( + source_session, + arguments.role_arn, + session_name, + region=region, + external_id=arguments.external_id, + role_duration=role_duration, + ) + return role_session + + target_profile = profiles.get(arguments.target_profile_name) + profile_lib.validate_profile(profiles, arguments.target_profile_name) + is_role = profile_lib.is_role(target_profile) + mfa_serial = profile_lib.get_mfa_serial(profiles, arguments.target_profile_name) + external_id = arguments.external_id or target_profile.get('external_id') + role_duration = arguments.role_duration or target_profile.get('duration_seconds') or int(config.get('role-duration')) + if not mfa_serial: + logger.debug('MFA is not required') + if not is_role: + logger.debug('No assume-role called and no mfa needed, returning profile credentials') + return_session = profile_lib.profile_to_credentials(target_profile) + return_session['Region'] = region + return return_session + else: + source_profile = profile_lib.get_source_profile(profiles, arguments.target_profile_name) + source_credentials = profile_lib.profile_to_credentials(source_profile) + role_session = aws_lib.assume_role( + source_credentials, + target_profile.get('role_arn'), + arguments.session_name or arguments.target_profile_name, + region=region, + external_id=external_id, + role_duration=role_duration, + ) + return role_session + else: + logger.debug('MFA is required') + if not is_role: + logger.debug('No assume_role call needed') + source_credentials = profile_lib.profile_to_credentials(target_profile) + user_session = aws_lib.get_session_token( + source_credentials, + region=region, + mfa_serial=mfa_serial, + mfa_token=arguments.mfa_token, + ignore_cache=arguments.force_refresh, + ) + return user_session + else: + logger.debug('assume_role call needed') + if role_duration: # cannot use temp creds with custom role duration + logger.debug('Skipping the get_session_token call, temp creds cannot be used for custom role duration') + source_profile = profile_lib.get_source_profile(profiles, arguments.target_profile_name) + source_session = profile_lib.profile_to_credentials(source_profile) + role_session = aws_lib.assume_role( + source_session, + target_profile.get('role_arn'), + arguments.session_name or arguments.target_profile_name, + region=region, + external_id=external_id, + role_duration=role_duration, + mfa_serial=mfa_serial, + mfa_token=arguments.mfa_token, + ) + return role_session + else: + logger.debug('Calling get_session_token to assume role with') + source_profile = profile_lib.get_source_profile(profiles, arguments.target_profile_name) + source_credentials = profile_lib.profile_to_credentials(source_profile) + source_session = aws_lib.get_session_token( + source_credentials, + region=region, + mfa_serial=mfa_serial, + mfa_token=arguments.mfa_token, + ignore_cache=arguments.force_refresh, + ) + role_session = aws_lib.assume_role( + source_session, + target_profile.get('role_arn'), + arguments.session_name or arguments.target_profile_name, + region=region, + external_id=external_id, + role_duration=role_duration, + ) + if arguments.auto_refresh: + _, credentials_file = aws_files_lib.get_aws_files(arguments, config) + autoawsume_profile_name = 'autoawsume-{}'.format(arguments.target_profile_name) + profile = profile_lib.credentials_to_profile(role_session) + profile['expiration'] = role_session.get('Expiration').strftime('%Y-%m-%d %H:%M:%S') + profile['source_expiration'] = source_session.get('Expiration').strftime('%Y-%m-%d %H:%M:%S') + profile['awsumepy_command'] = ' '.join(arguments.system_arguments) + aws_files_lib.add_section(autoawsume_profile_name, profile, credentials_file, True) + return role_session + + +@hookimpl +def get_profile_names(config: dict, arguments: argparse.Namespace): + return ['profile1', 'profile2', 'profile3'] diff --git a/awsume/awsumepy/hookimpl.py b/awsume/awsumepy/hookimpl.py new file mode 100644 index 0000000..caba973 --- /dev/null +++ b/awsume/awsumepy/hookimpl.py @@ -0,0 +1,5 @@ +import pluggy + +hookimpl = pluggy.HookimplMarker('awsume') + +"""Marker to be imported and used in plugins (and for own implementations)""" diff --git a/awsume/awsumepy/hookspec.py b/awsume/awsumepy/hookspec.py new file mode 100644 index 0000000..c4101f0 --- /dev/null +++ b/awsume/awsumepy/hookspec.py @@ -0,0 +1,77 @@ +import pluggy +import argparse + +hookspec = pluggy.HookspecMarker('awsume') + + +@hookspec +def pre_add_arguments(config: dict): + """pre add_arguments hook""" + +@hookspec +def add_arguments(config: dict, parser: argparse.ArgumentParser): + """add argparse arguments to the parser, should try/except for conflicting arguments""" + +@hookspec +def post_add_arguments(config: dict, arguments: argparse.Namespace, parser: argparse.ArgumentParser): + """post add_arguments hook""" + + + +@hookspec +def pre_collect_aws_profiles(config: dict, arguments: argparse.Namespace, credentials_file: str, config_file: str): + """""" + +@hookspec +def collect_aws_profiles(config: dict, arguments: argparse.Namespace, credentials_file: str, config_file: str): + """""" + +@hookspec +def post_collect_aws_profiles(config: dict, arguments: argparse.Namespace, profiles: dict): + """""" + + + +@hookspec +def pre_assume_role(config: dict, arguments: argparse.Namespace, profiles: dict): + """""" + +@hookspec +def assume_role(config: dict, arguments: argparse.Namespace, profiles: dict): + """""" + +@hookspec +def assume_role_with_saml(config: dict, arguments: argparse.Namespace): + """""" + +@hookspec +def assume_role_with_web_identity(config: dict, arguments: argparse.Namespace): + """""" + +@hookspec +def post_assume_role(config: dict, arguments: argparse.Namespace, profiles: dict, credentials: dict): + """""" + + + +@hookspec +def catch_profile_not_found_exception(config: dict, arguments: argparse.Namespace, profiles: dict): + """""" + +@hookspec +def catch_invalid_profile_exception(config: dict, arguments: argparse.Namespace, profiles: dict): + """""" + +@hookspec +def catch_user_authentication_error(config: dict, arguments: argparse.Namespace, profiles: dict): + """""" + +@hookspec +def catch_role_authentication_error(config: dict, arguments: argparse.Namespace, profiles: dict): + """""" + + + +@hookspec +def get_profile_names(config: dict, arguments: argparse.Namespace): + """""" diff --git a/awsume/awsumepy/main.py b/awsume/awsumepy/main.py new file mode 100644 index 0000000..30182ff --- /dev/null +++ b/awsume/awsumepy/main.py @@ -0,0 +1,36 @@ +import sys +import argparse +import pluggy +import signal +import logging + +from .. import __data__ +from . import hookspec +from . import default_plugins +from . import app +from . lib.logger import logger +from . lib.safe_print import safe_print + + +# remove traceback on ctrl+C +def __exit_awsume(arg1, arg2): # pragma: no cover + """Make sure ^C doesn't spam the terminal.""" + print('') + sys.exit(1) +signal.signal(signal.SIGINT, __exit_awsume) + + +def run_awsume(argument_list): + awsume = app.Awsume() + awsume.run(argument_list) + + +def main(): + if '--debug' in sys.argv: + logger.setLevel(logging.DEBUG) + logger.debug('Debug logs are visible') + elif '--info' in sys.argv: + logger.setLevel(logging.INFO) + logger.info('Info logs are visible') + logger.debug('Executing awsume') + run_awsume(sys.argv) diff --git a/awsume/configure/__init__.py b/awsume/configure/__init__.py new file mode 100644 index 0000000..b6e690f --- /dev/null +++ b/awsume/configure/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/awsume/configure/alias.py b/awsume/configure/alias.py new file mode 100644 index 0000000..67cd1ad --- /dev/null +++ b/awsume/configure/alias.py @@ -0,0 +1,17 @@ +import os, pathlib +from distutils.spawn import find_executable + +DEFAULT_ALIAS = 'alias awsume=". awsume"' +PYENV_ALIAS = r'alias awsume=". \$(pyenv which awsume)"' + +def main(shell: str, alias_file: str): + alias = PYENV_ALIAS if find_executable('pyenv') else DEFAULT_ALIAS + alias_file = pathlib.Path(alias_file).expanduser() + if alias in open(alias_file, 'r').read(): + print('Alias already in ' + alias_file) + else: + with open(alias_file, 'a') as f: + f.write('\n#AWSume alias to source the AWSume script\n') + f.write(alias) + f.write('\n') + print('Wrote alias to ' + alias_file) diff --git a/awsume/configure/autocomplete.py b/awsume/configure/autocomplete.py new file mode 100644 index 0000000..4ed0778 --- /dev/null +++ b/awsume/configure/autocomplete.py @@ -0,0 +1,49 @@ +import os, pathlib +from distutils.spawn import find_executable + +BASH_AUTOCOMPLETE_SCRIPT = """ +_awsume() { + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts=$(awsumepy --rolesusers) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} +complete -F _awsume awsume +""" + +ZSH_AUTOCOMPLETE_SCRIPT = """ +#compdef awsume +_arguments "*: :($(awsumepy --rolesusers))" +""" + +POWERSHELL_AUTOCOMPLETE_SCRIPT = """ +Register-ArgumentCompleter -Native -CommandName awsume -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + $(awsumepy --rolesusers) | + Where-Object { $_ -like "$wordToComplete*" } | + Sort-Object | + ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} +""" + +SCRIPTS = { + 'bash': BASH_AUTOCOMPLETE_SCRIPT, + 'zsh': ZSH_AUTOCOMPLETE_SCRIPT, + 'powershell': POWERSHELL_AUTOCOMPLETE_SCRIPT, +} + +def main(shell: str, autocomplete_file: str): + autocomplete_file = pathlib.Path(autocomplete_file).expanduser() + autocomplete_script = SCRIPTS[shell] + if autocomplete_script in open(autocomplete_file, 'r').read(): + print('Autocomplete script already in ' + autocomplete_file) + else: + with open(autocomplete_file, 'a') as f: + f.write('\n#Auto-Complete function for AWSume') + f.write(autocomplete_script) + print('Wrote autocomplete script to ' + autocomplete_file) diff --git a/awsume/configure/main.py b/awsume/configure/main.py new file mode 100644 index 0000000..f692591 --- /dev/null +++ b/awsume/configure/main.py @@ -0,0 +1,44 @@ +import argparse, os, sys +from . import alias, autocomplete + + +def parse_args(argv: sys.argv) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument('--shell', + dest='shell', + metavar='shell', + help='The shell you will use awsume under', + required=True, + choices=['bash', 'zsh', 'powershell'] + ) + parser.add_argument('--autocomplete-file', + dest='autocomplete_file', + metavar='autocomplete_file', + required=True, + help='The file you want the autocomplete script to be defined in', + ) + parser.add_argument('--alias-file', + default=None, + dest='alias_file', + metavar='alias_file', + required=False, + help='The file you want the alias to be defined in', + ) + args = parser.parse_args(argv) + + if args.shell in ['powershell'] and args.alias_file: + parser.error('No alias file is needed for shell: powershell') + + return args + + +def run(shell: str, alias_file: str, autocomplete_file: str): + if alias_file: + alias.main(shell, alias_file) + if autocomplete_file: + autocomplete.main(shell, autocomplete_file) + + +def main(): + args = parse_args(sys.argv[1:]) + run(args.shell, args.alias_file, args.autocomplete_file) diff --git a/awsume/configure/post_install.py b/awsume/configure/post_install.py new file mode 100644 index 0000000..35ca25b --- /dev/null +++ b/awsume/configure/post_install.py @@ -0,0 +1,45 @@ +import os, subprocess, sys +from pathlib import Path +from setuptools.command.install import install +from distutils.spawn import find_executable + +from .main import run + +BASH_LOGIN_FILES = ['~/.bash_profile', '~/.bash_login', '~/.profile', '~/.bashrc'] + + +class CustomInstall(install): + def get_bash_file(self) -> str: + paths = [Path(_).expanduser() for _ in BASH_LOGIN_FILES] + result = [_ for _ in paths if os.path.exists(_) and os.path.isfile(_) and os.access(_, os.R_OK)] + if not result: + open('~/.bash_profile', 'w').close() + result = [Path('~/.bash_profile').expanduser()] + return result[0] + + def get_zsh_file(self) -> str: + z_dot_dir = os.environ.get('ZDOTDIR', '~') + zsh_file = Path(z_dot_dir + '/.zshenv').expanduser() + if not os.path.exists(zsh_file) or not os.path.isfile(zsh_file): + open(zsh_file, 'w').close() + return zsh_file + + def run(self): + install.run(self) + if os.environ.get('AWSUME_SKIP_ALIAS_SETUP'): + print('===== Skipping Alias Setup =====') + return + if find_executable('bash'): + print('===== Setting up bash =====') + bash_file = self.get_bash_file() + run('bash', bash_file, bash_file) + if find_executable('zsh'): + print('===== Setting up zsh =====') + zsh_file = self.get_zsh_file() + run('zsh', zsh_file, zsh_file) + if find_executable('powershell'): + print('===== Setting up powershell =====') + (powershell_file, _) = subprocess.Popen(['powershell', '$profile'], stdout=subprocess.PIPE, shell=True).communicate() + powershell_file = str(powershell_file.decode('ascii')).replace('\r\n', '') + run('powershell', None, powershell_file) + print('===== Finished setting up =====', file=sys.stderr) diff --git a/awsume/shellScripts/__init__.py b/awsume/shellScripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/awsume/shellScripts/awsume b/awsume/shellScripts/awsume deleted file mode 100755 index 028598c..0000000 --- a/awsume/shellScripts/awsume +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash -#AWSume - a bash script shell wrapper to awsumepy, a cli that makes using AWS IAM credentials easy - -#AWSUME_FLAG - what awsumepy told the shell to do -#AWSUME_n - the data from awsumepy -read AWSUME_FLAG AWSUME_1 AWSUME_2 AWSUME_3 AWSUME_4 AWSUME_5 AWSUME_6 <<< $(awsumepy "$@") -# remove carraige return -AWSUME_FLAG=$(echo "$AWSUME_FLAG" | tr -d '\r') - -#if incorrect flag/help -if [ "$AWSUME_FLAG" = "usage:" ]; then - awsumepy -h -#if version check -elif [ "$AWSUME_FLAG" = "Version" ]; then - awsumepy -v -#if -l flag passed -elif [ "$AWSUME_FLAG" = "Listing..." ]; then - awsumepy -l -#set up auto-refreshing role -elif [ "$AWSUME_FLAG" = "Auto" ]; then - unset AWS_SECRET_ACCESS_KEY - unset AWS_SESSION_TOKEN - unset AWS_SECURITY_TOKEN - unset AWS_ACCESS_KEY_ID - unset AWS_REGION - unset AWS_DEFAULT_REGION - unset AWS_PROFILE - unset AWS_DEFAULT_PROFILE - unset AWSUME_PROFILE - - #set the profile that will contain the session credentials - export AWS_PROFILE=$AWSUME_1 - export AWS_DEFAULT_PROFILE=$AWSUME_1 - - if [ ! "$AWSUME_2" = "None" ]; then - export AWS_REGION=$AWSUME_2 - export AWS_DEFAULT_REGION=$AWSUME_2 - fi - - export AWSUME_PROFILE=$AWSUME_3 - #run the background autoawsume process - autoawsume & disown - -#user sent the unset variables flag -elif [ "$AWSUME_FLAG" = "Unset" ]; then - unset AWS_PROFILE - unset AWS_DEFAULT_PROFILE - unset AWS_SECRET_ACCESS_KEY - unset AWS_SESSION_TOKEN - unset AWS_SECURITY_TOKEN - unset AWS_ACCESS_KEY_ID - unset AWS_REGION - unset AWS_DEFAULT_REGION - unset AWSUME_PROFILE - - #show the commands to unset these environment variables - for AWSUME_var in "$@" - do - #show commands - if [[ "$AWSUME_var" == "-s"* ]]; then - echo unset AWS_PROFILE - echo unset AWS_DEFAULT_PROFILE - echo unset AWS_SECRET_ACCESS_KEY - echo unset AWS_SESSION_TOKEN - echo unset AWS_SECURITY_TOKEN - echo unset AWS_ACCESS_KEY_ID - echo unset AWS_REGION - echo unset AWS_DEFAULT_REGION - echo unset AWSUME_PROFILE - fi - done - return - -#user sent the kill flag, so do no more -elif [ "$AWSUME_FLAG" = "Kill" ]; then - unset AWS_PROFILE - unset AWS_DEFAULT_PROFILE - unset AWS_SECRET_ACCESS_KEY - unset AWS_SESSION_TOKEN - unset AWS_SECURITY_TOKEN - unset AWS_ACCESS_KEY_ID - unset AWS_REGION - unset AWS_DEFAULT_REGION - unset AWSUME_PROFILE - return - -elif [ "$AWSUME_FLAG" = "Stop" ]; then - if [ "auto-refresh-$AWSUME_1" == "$AWS_PROFILE" ]; then - unset AWS_PROFILE - unset AWS_DEFAULT_PROFILE - fi - return - -#awsume the profile -elif [ "$AWSUME_FLAG" = "Awsume" ]; then - unset AWS_SECRET_ACCESS_KEY - unset AWS_SESSION_TOKEN - unset AWS_SECURITY_TOKEN - unset AWS_ACCESS_KEY_ID - unset AWS_REGION - unset AWS_DEFAULT_REGION - unset AWS_PROFILE - unset AWS_DEFAULT_PROFILE - unset AWSUME_PROFILE - - export AWS_ACCESS_KEY_ID=$AWSUME_1 - export AWS_SECRET_ACCESS_KEY=$AWSUME_2 - - if [ ! "$AWSUME_3" = "None" ]; then - export AWS_SESSION_TOKEN=$AWSUME_3 - export AWS_SECURITY_TOKEN=$AWSUME_3 - fi - - if [ ! "$AWSUME_4" = "None" ]; then - export AWS_REGION=$AWSUME_4 - export AWS_DEFAULT_REGION=$AWSUME_4 - fi - - export AWSUME_PROFILE=$AWSUME_5 - - #if enabled, show the exact commands to use in order to assume the role we just assumed - for AWSUME_var in "$@" - do - #show commands - if [[ "$AWSUME_var" == "-s"* ]]; then - echo export AWS_ACCESS_KEY_ID=$AWSUME_1 - echo export AWS_SECRET_ACCESS_KEY=$AWSUME_2 - - if [ ! "$AWSUME_3" = "None" ]; then - echo export AWS_SESSION_TOKEN=$AWSUME_3 - echo export AWS_SECURITY_TOKEN=$AWSUME_3 - fi - - if [ ! "$AWSUME_4" = "None" ]; then - echo export AWS_REGION=$AWSUME_4 - echo export AWS_DEFAULT_REGION=$AWSUME_4 - fi - - echo export AWSUME_PROFILE=$AWSUME_5 - - fi - done -fi diff --git a/awsume/test_autoawsume.py b/awsume/test_autoawsume.py deleted file mode 100644 index e21df7c..0000000 --- a/awsume/test_autoawsume.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Test autoawsume""" -import unittest -import sys -import os -import datetime -import imp -import mock -import botocore - -sys.path.append(os.path.dirname(sys.path[0])) - -AUTOAWSUME = imp.load_source('autoawsume', 'awsume/autoawsume.py') - -class TestCommandLineArgumentHandling(unittest.TestCase): - - @mock.patch('awsume.awsumepy.write_auto_awsume_session') - @mock.patch('awsume.awsumepy.create_sts_client') - @mock.patch('awsume.awsumepy.read_aws_cache') - def test_refresh_session(self, - mock_read_cache, - mock_create_client, - mock_write_auto_session): - """Test the refresh_session autoawsume function""" - mock_client = mock.Mock() - mock_client.assume_role = mock.Mock() - fake_response = { - 'Credentials': { - 'Expiration': mock.Mock(), - 'AccessKeyId':'EXAMPLE', - 'SecretAccessKey':'EXAMPLE', - 'SessionToken':'EXAMPLE', - } - } - mock_client.assume_role.return_value = fake_response - mock_create_client.return_value = mock_client - fake_auto_profile = { - '__name__':'EXAMPLE', - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE', - 'aws_session_token':'EXAMPLE', - 'awsume_role_expiration':'EXAMPLE', - 'awsume_user_expiration':'EXAMPLE', - 'awsume_session_name':'EXAMPLE', - 'awsume_cache_name':'EXAMPLE', - 'aws_role_arn':'EXAMPLE', - } - fake_cache_credentials = { - 'AccessKeyId':'EXAMPLE', - 'SecretAccessKey':'EXAMPLE', - 'SessionToken':'EXAMPLE', - 'region':'EXAMPLE', - } - mock_read_cache.return_value = fake_cache_credentials - AUTOAWSUME.refresh_session(fake_auto_profile) - mock_write_auto_session.assert_called_once() - - mock_client.assume_role.side_effect = [botocore.exceptions.ClientError({}, {})] - AUTOAWSUME.refresh_session(fake_auto_profile) - - def test_extract_auto_refresh_profiles(self): - """Test the extract_auto_refresh_profiles autoawsume function""" - fake_profiles = { - 'some-profile': {}, - 'some-other-profile': {}, - 'auto-refresh-some-profile': {}, - 'auto-refresh-some-other-profile': {}, - } - auto_profiles = AUTOAWSUME.extract_auto_refresh_profiles(fake_profiles) - self.assertEqual(auto_profiles, { - 'auto-refresh-some-profile': {}, - 'auto-refresh-some-other-profile': {}, - }) - - @mock.patch('autoawsume.get_now') - def test_get_earliest_expiration(self, mock_now): - """Test the get_earliest_expiration autoawsume function""" - mock_now.return_value = 'current time' - fake_auto_profiles = {} - earliest_expiration = AUTOAWSUME.get_earliest_expiration(fake_auto_profiles) - self.assertEqual('current time', earliest_expiration) - - fake_auto_profiles = { - 'profile1': { - 'awsume_user_expiration':'2018-06-15 12:24:38', - 'awsume_role_expiration':'2018-06-15 12:24:15', - }, - 'profile2': { - 'awsume_user_expiration':'2018-06-15 12:24:20', - 'awsume_role_expiration':'2018-06-15 12:24:17', - }, - 'profile3': { - 'awsume_user_expiration':'2018-06-15 12:24:02', - 'awsume_role_expiration':'2018-06-15 12:24:54', - }, - } - earliest_expiration = AUTOAWSUME.get_earliest_expiration(fake_auto_profiles) - self.assertEqual( - datetime.datetime.strptime('2018-06-15 12:24:02', '%Y-%m-%d %H:%M:%S'), - earliest_expiration) - - @mock.patch('awsume.awsumepy.remove_auto_profile') - @mock.patch('autoawsume.refresh_session') - @mock.patch('autoawsume.get_now') - def test_refresh_expired_profiles(self, - mock_now, - mock_refresh, - mock_remove): - """Test the refresh_expired_profiles autoawsume function""" - mock_now.return_value = datetime.datetime.strptime( - '2018-06-15 12:24:30', - '%Y-%m-%d %H:%M:%S' - ) - fake_auto_profiles = { - 'profile1': { - '__name__':'profile1', - 'awsume_user_expiration':'2018-06-15 12:24:59',#Good - 'awsume_role_expiration':'2018-06-15 12:24:59',#Good - }, - 'profile2': { - '__name__':'profile2', - 'awsume_user_expiration':'2018-06-15 12:24:00',#Expired - 'awsume_role_expiration':'2018-06-15 12:24:59',#Good - }, - 'profile3': { - '__name__':'profile3', - 'awsume_user_expiration':'2018-06-15 12:24:59',#Good - 'awsume_role_expiration':'2018-06-15 12:24:00',#Expired - }, - } - AUTOAWSUME.refresh_expired_profiles(fake_auto_profiles) - mock_refresh.assert_called_once() - mock_remove.assert_called_once() - - @mock.patch('time.sleep') - @mock.patch('autoawsume.get_now') - @mock.patch('autoawsume.get_earliest_expiration') - @mock.patch('autoawsume.refresh_expired_profiles') - @mock.patch('autoawsume.extract_auto_refresh_profiles') - @mock.patch('awsume.awsumepy.read_ini_file') - def test_main(self, - mock_read_ini_file, - mock_extract_auto_profiles, - mock_refresh, - mock_get_earliest_expiration, - mock_now, - mock_sleep): - """Test the main autoawsume function""" - mock_read_ini_file.return_value = {} - mock_extract_auto_profiles.return_value = {} - mock_get_earliest_expiration.return_value = datetime.datetime(2018, 6, 15, 12, 24, 30) - mock_now.side_effect = [ - datetime.datetime(2018, 6, 15, 12, 24, 0), - datetime.datetime(2018, 6, 15, 12, 24, 30), - ] - AUTOAWSUME.main() - mock_sleep.assert_called_once_with(30) - -if __name__ == '__main__': - unittest.main() diff --git a/awsume/test_awsumepy.py b/awsume/test_awsumepy.py deleted file mode 100644 index 091dd7e..0000000 --- a/awsume/test_awsumepy.py +++ /dev/null @@ -1,1256 +0,0 @@ -"""Test Awsumepy""" -import datetime -import unittest -import imp -import mock - -AWSUMEPY = imp.load_source('awsumepy', 'awsume/awsumepy.py') - - - -# -# TestCommandLineArgumentHandling -# -class TestCommandLineArgumentHandling(unittest.TestCase): - """Test suite for CommandLineArgumentHandling""" - @mock.patch('argparse.ArgumentParser') - def test_generate_argument_parser(self, mock_arg_parser): - """test generate_argument_parser awsumepy function""" - AWSUMEPY.generate_argument_parser() - mock_arg_parser.assert_called_once() - - def test_add_arguments(self): - """test add_arguments awsumepy function""" - mock_add_argument = mock.Mock() - mock_argument_parser = mock.Mock() - mock_argument_parser.add_argument = mock_add_argument - AWSUMEPY.add_arguments(mock_argument_parser) - mock_add_argument.assert_called() - - def test_parse_args(self): - """test parse_args awsumepy function""" - mock_parse_args = mock.Mock() - mock_argument_parser = mock.Mock() - mock_argument_parser.parse_args = mock_parse_args - system_arguments = ['sys', 'args'] - AWSUMEPY.parse_args(mock_argument_parser, system_arguments) - mock_parse_args.assert_called_with(system_arguments) - - - -# -# TestReadAWSFiles -# -class TestReadAWSFiles(unittest.TestCase): - """Test suite for ReadAWSFiles""" - @mock.patch('six.moves.builtins.print') - @mock.patch('six.moves.configparser.ConfigParser') - @mock.patch('os.path.exists') - def test_read_ini_file(self, - mock_path_exists, - mock_config_parser, - mock_print): - """test read_ini_file awsumepy function""" - mock_path_exists.return_value = True - mock_config_object = mock.Mock() - mock_config_object.read = mock.Mock() - mock_config_parser.return_value = mock_config_object - mock_config_object.sections = mock.Mock() - mock_config_object.sections.return_value = ['first-profile', 'profile next-profile'] - mock_config_object.options = mock.Mock() - mock_config_object.options.side_effect = [['key'], ['key2']] - mock_config_object.get = mock.Mock() - mock_config_object.get.side_effect = ['value', 'value'] - profiles = AWSUMEPY.read_ini_file('path') - mock_config_object.read.assert_called_once_with('path') - self.assertEqual(profiles, { - 'first-profile': { - '__name__':'first-profile', - 'key':'value' - }, - 'next-profile': { - '__name__':'next-profile', - 'key2':'value' - } - }) - - mock_path_exists.return_value = False - AWSUMEPY.read_ini_file('path') - mock_print.assert_called_once() - - def test_merge_profile(self): - """test merge_role_and_source_profile awsumepy function""" - user_profile = { - '__name__': 'user_profile', - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE' - } - role_profile = { - '__name__': 'role_profile', - 'source_profile':'EXAMPLE', - 'role_arn':'EXAMPLE' - } - AWSUMEPY.merge_role_and_source_profile(role_profile, user_profile) - self.assertIsNotNone(role_profile.get('aws_access_key_id')) - self.assertIsNotNone(role_profile.get('aws_secret_access_key')) - - user_profile = { - '__name__': 'user_profile', - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE', - 'mfa_serial':'EXAMPLE', - 'region':'EXAMPLE', - } - role_profile = { - '__name__': 'role_profile', - 'source_profile':'EXAMPLE', - 'role_arn':'EXAMPLE', - } - AWSUMEPY.merge_role_and_source_profile(role_profile, user_profile) - self.assertIsNotNone(role_profile.get('aws_access_key_id')) - self.assertIsNotNone(role_profile.get('aws_secret_access_key')) - self.assertIsNotNone(role_profile.get('mfa_serial')) - self.assertIsNotNone(role_profile.get('region')) - - @mock.patch('six.moves.builtins.print') - def test_mix_profiles(self, mock_print): - """test mix_role_and_source_profiles awsumepy function""" - fake_profiles = { - 'client-dev-role': { - '__name__': 'client-dev-role', - 'role_arn': 'EXAMPLE', - 'source_profile': 'client' - }, - 'client': { - '__name__': 'client', - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE' - }, - 'client-prod-role': { - '__name__': 'client-prod-role', - 'role_arn': 'EXAMPLE', - 'source_profile': 'client' - }, - } - AWSUMEPY.mix_role_and_source_profiles(fake_profiles) - self.assertIsNotNone(fake_profiles['client-dev-role'].get('aws_access_key_id')) - self.assertIsNotNone(fake_profiles['client-dev-role'].get('aws_secret_access_key')) - self.assertIsNotNone(fake_profiles['client-prod-role'].get('aws_access_key_id')) - self.assertIsNotNone(fake_profiles['client-prod-role'].get('aws_secret_access_key')) - - fake_profiles = { - 'client-dev-role': { - 'role_arn': 'EXAMPLE', - 'source_profile': 'missing-profile' - }, - } - with self.assertRaises(SystemExit): - AWSUMEPY.mix_role_and_source_profiles(fake_profiles) - mock_print.assert_called() - - @mock.patch('awsumepy.read_ini_file') - def test_get_aws_profiles(self, mock_read_ini_file): - """test get_aws_profiles awsumepy function""" - mock_config_profiles = { - 'client-dev': { - 'key3':'value3', - 'key4':'value4' - } - } - mock_credentials_profiles = { - 'client-dev': { - 'key1':'value1', - 'key2':'value2' - }, - 'auto-refresh-client-dev': { - 'key1':'value1', - 'key2':'value2' - }, - } - mock_read_ini_file.side_effect = [mock_config_profiles, mock_credentials_profiles] - mock_app = None - mock_arguments = None - mock_profiles = AWSUMEPY.get_aws_profiles(mock_app, mock_arguments, '/config/path', '/credentials/path') - self.assertEqual(mock_profiles, { - 'client-dev': { - 'key1':'value1', - 'key2':'value2', - 'key3':'value3', - 'key4':'value4' - } - }) - self.assertFalse('auto-refresh-client-dev' in mock_credentials_profiles) - - def test_trim_auto_profiles(self): - """test trim_auto_profiles awsumepy function""" - fake_profiles = { - 'auto-refresh-client-profile': { - 'key':'value' - }, - 'client-profile': { - 'key':'value' - }, - 'auto-refresh-internal-profile': { - 'key':'value' - }, - 'internal-profile': { - 'key':'value' - }, - } - AWSUMEPY.trim_auto_profiles(fake_profiles) - self.assertFalse('auto-refresh-client-profile' in fake_profiles) - self.assertFalse('auto-refresh-internal-profile' in fake_profiles) - self.assertTrue('internal-profile' in fake_profiles) - self.assertTrue('client-profile' in fake_profiles) - - - -# -# TestListingProfiles -# -class TestListingProfiles(unittest.TestCase): - """Test suite for ListingProfiles""" - def test_get_account_id(self): - """test get_account_id awsumepy function""" - fake_profile_with_role_arn = { - 'role_arn': 'arn:aws:iam::8675309:role/dev-role' - } - fake_profile_with_mfa = { - 'mfa_serial': 'arn:aws:iam::123456789012:mfa/admin' - } - fake_profile_with_nothing = { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE' - } - self.assertEqual(AWSUMEPY.get_account_id(fake_profile_with_mfa), '123456789012') - self.assertEqual(AWSUMEPY.get_account_id(fake_profile_with_role_arn), '8675309') - self.assertEqual(AWSUMEPY.get_account_id(fake_profile_with_nothing), 'Unavailable') - - def test_format_aws_profiles(self): - """test format_aws_profiles awsumepy function""" - fake_profiles = { - 'client-dev-role': { - 'role_arn': 'EXAMPLE', - 'source_profile': 'client', - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - }, - 'client': { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - }, - 'client-prod-role': { - 'role_arn': 'EXAMPLE', - 'source_profile': 'client', - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - }, - } - fake_profile_list = AWSUMEPY.format_aws_profiles(fake_profiles) - flattened_list = [item for sublist in fake_profile_list for item in sublist] - self.assertTrue('client-dev-role' in flattened_list) - self.assertTrue('client-prod-role' in flattened_list) - self.assertTrue('client' in flattened_list) - self.assertTrue('EXAMPLE' in flattened_list) - - @mock.patch('awsumepy.print_formatted_data') - @mock.patch('awsumepy.format_aws_profiles') - def test_list_profile_data(self, - mock_format_aws_profiles, - mock_print_formatted_data): - """test list_profile_data awsumepy function""" - AWSUMEPY.list_profile_data({}) - mock_format_aws_profiles.assert_called() - mock_print_formatted_data.assert_called() - - @mock.patch('awsumepy.mix_role_and_source_profiles') - @mock.patch('awsumepy.get_aws_profiles') - def test_get_profile_names(self, - mock_get_aws_profiles, - mock_mix_profiles): - """test get_profile_names awsumepy function""" - fake_profiles = { - 'client-dev-role': { - 'role_arn': 'EXAMPLE', - 'source_profile': 'client', - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - }, - 'client': { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - }, - 'client-prod-role': { - 'role_arn': 'EXAMPLE', - 'source_profile': 'client', - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - }, - } - mock_get_aws_profiles.return_value = fake_profiles - fake_profile_names = AWSUMEPY.get_profile_names(None, None) - self.assertTrue('client-dev-role' in fake_profile_names) - self.assertTrue('client' in fake_profile_names) - self.assertTrue('client-prod-role' in fake_profile_names) - - @mock.patch('six.moves.builtins.print') - def test_list_profile_names(self, mock_print): - """Test list_profile_names awsumepy function""" - fake_args = mock.Mock() - fake_app = mock.Mock() - plugin_func1 = mock.Mock() - plugin_func1.return_value = [ - 'profile1-dev', - 'profile1-prod', - 'profile1-internal', - ] - plugin_func2 = mock.Mock() - plugin_func2.return_value = [ - 'profile2-dev', - 'profile2-prod', - 'profile2-internal', - ] - fake_app.awsumeFunctions = { - 'get_profile_names':[plugin_func1, plugin_func2] - } - AWSUMEPY.list_profile_names(fake_args, fake_app) - mock_print.assert_called_once_with('profile1-dev\nprofile1-prod\nprofile1-internal\nprofile2-dev\nprofile2-prod\nprofile2-internal') - - - - -# -# TestInspectionAndValidation -# -class TestInspectionAndValidation(unittest.TestCase): - """Test suite for InspectionAndValidation""" - @mock.patch('awsumepy.is_role') - def test_valid_profile(self, mock_is_role): - """test valid_profile awsumepy function""" - valid_user = { - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE' - } - valid_role = { - 'source_profile':'EXAMPLE', - 'role_arn':'EXAMPLE' - } - mock_missing_access_key_id = { - 'aws_secret_access_key':'EXAMPLE' - } - mock_missing_secret_access_key = { - 'aws_access_key_id':'EXAMPLE' - } - mock_is_role.return_value = True - self.assertTrue(AWSUMEPY.valid_profile(valid_user)) - self.assertTrue(AWSUMEPY.valid_profile(valid_role)) - mock_is_role.return_value = False - self.assertFalse(AWSUMEPY.valid_profile(mock_missing_access_key_id)) - self.assertFalse(AWSUMEPY.valid_profile(mock_missing_secret_access_key)) - - def test_requires_mfa(self): - """test requires_mfa awsumepy function""" - mfa_profile = { - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE', - 'mfa_serial':'EXAMPLE' - } - no_mfa_profile = { - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE' - } - self.assertTrue(AWSUMEPY.requires_mfa(mfa_profile)) - self.assertFalse(AWSUMEPY.requires_mfa(no_mfa_profile)) - - def test_is_role(self): - """test is_role awsumepy function""" - user_profile = { - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE', - } - role_profile = { - 'source_profile':'EXAMPLE', - 'role_arn':'EXAMPLE' - } - self.assertTrue(AWSUMEPY.is_role(role_profile)) - self.assertFalse(AWSUMEPY.is_role(user_profile)) - - def test_valid_mfa_token(self): - """test valid_mfa_token awsumepy function""" - valid_tokens = [ - '000000', - '999999', - '123456' - ] - invalid_tokens = [ - '12345', - '1234567', - '12345a', - ' ', - '', - 'abcdef' - ] - for token in valid_tokens: - self.assertTrue(AWSUMEPY.valid_mfa_token(token)) - for token in invalid_tokens: - self.assertFalse(AWSUMEPY.valid_mfa_token(token)) - - def test_valid_cache_session(self): - """test valid_cache_session awsumepy function""" - valid_session = { - 'Expiration': '9999-12-31 11:59:59', - } - invalid_session = { - - } - expired_session = { - 'Expiration': datetime.datetime.min - } - self.assertTrue(AWSUMEPY.valid_cache_session(valid_session)) - self.assertFalse(AWSUMEPY.valid_cache_session(expired_session)) - self.assertFalse(AWSUMEPY.valid_cache_session(invalid_session)) - - def test_fix_session_credentials(self): - """test fix_session_credentials awsumepy function""" - mock_as_timezone = mock.Mock() - fake_profiles = { - 'default':{ - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE', - 'mfa_serial':'EXAMPLE', - 'region':'DefaultRegion' - }, - 'client':{ - 'aws_access_key_id':'EXAMPLE', - 'aws_secret_access_key':'EXAMPLE', - } - } - fake_session = { - 'Expiration':mock.Mock() - } - fake_args = mock.Mock() - fake_args.target_profile_name = 'client' - fake_session['Expiration'].astimezone = mock_as_timezone - - AWSUMEPY.fix_session_credentials(fake_session, fake_profiles, fake_args) - self.assertTrue(fake_session['region'] == 'DefaultRegion') - - fake_profiles['client']['region'] = 'ClientRegion' - AWSUMEPY.fix_session_credentials(fake_session, fake_profiles, fake_args) - self.assertTrue(fake_session['region'] == 'ClientRegion') - mock_as_timezone.assert_called() - - - -# -# TestInputOutput -# -class TestInputOutput(unittest.TestCase): - """Test suite for InputOutput""" - @mock.patch('awsumepy.valid_mfa_token') - @mock.patch('awsumepy.get_input') - def test_read_mfa(self, mock_input, mock_valid_mfa_token): - """test read_mfa awsumepy function""" - mock_valid_mfa_token.side_effect = [ - False, - False, - False, - True - ] - mock_input.side_effect = [ - 'invalid', - 'invalid', - 'invalid', - 'validmfa' - ] - token = AWSUMEPY.read_mfa() - self.assertEqual(token, 'validmfa') - - - -# -# TestCachingSessions -# -class TestCachingSessions(unittest.TestCase): - """Test suite for CachingSessions""" - @mock.patch('six.moves.builtins.open') - @mock.patch('json.load') - @mock.patch('os.path.isfile') - def test_read_aws_cache(self, - mock_os_path_isfile, - mock_json_load, - mock_open): - """test read_aws_cache awsumepy function""" - mock_os_path_isfile.return_value = True - mock_json_load.return_value = {'Expiration': '1999-12-31 11:59:59'} - session = AWSUMEPY.read_aws_cache('/cache/path/', 'cache-file') - self.assertEqual(session, { - 'Expiration': '1999-12-31 11:59:59' - }) - mock_os_path_isfile.return_value = False - session = AWSUMEPY.read_aws_cache('/cache/path/', 'cache-file') - self.assertEqual(session, {}) - - mock_os_path_isfile.return_value = True - mock_open.side_effect = Exception - session = AWSUMEPY.read_aws_cache('/cache/path/', 'cache-file') - self.assertEqual(session, {}) - - @mock.patch('os.makedirs') - @mock.patch('os.path.exists') - @mock.patch('six.moves.builtins.open') - @mock.patch('json.dump') - def test_write_aws_cache(self, - mock_json_dump, - mock_open, - mock_path_exists, - mock_makedirs): - """test write_aws_cache awsumepy function""" - mock_path_exists.return_value = True - AWSUMEPY.write_aws_cache('/cache/path/', 'cache-file', {'session':'credentials'}) - mock_json_dump.assert_called() - - mock_path_exists.return_value = False - AWSUMEPY.write_aws_cache('/cache/path/', 'cache-file', {'session':'credentials'}) - mock_makedirs.assert_called() - - - -# -# TestAwsumeWorkflow -# -class TestAwsumeWorkflow(unittest.TestCase): - """Test suite for AwsumeWorkflow""" - @mock.patch('awsumepy.config_help') - @mock.patch('awsumepy.display_plugin_info') - @mock.patch('awsumepy.delete_plugin') - @mock.patch('awsumepy.download_plugin') - @mock.patch('awsumepy.list_profile_names') - @mock.patch('awsumepy.kill') - def test_pre_awsume(self, - mock_kill, - mock_list_profile_names, - mock_download_plugin, - mock_delete_plugin, - mock_display_plugin_info, - mock_config_help): - """test pre_awsume awsumepy function""" - fake_args = mock.Mock() - fake_app = mock.Mock() - fake_app.set_option = mock.Mock() - fake_args.version = False - fake_args.profile_name = None - fake_args.target_profile_name = None - fake_args.kill = False - fake_args.list_profile_names = False - fake_args.plugin_urls = None - fake_args.delete_plugin_name = None - fake_args.display_plugin_info = False - fake_args.info = False - fake_args.debug = False - fake_args.config = None - fake_args.config_help = False - - AWSUMEPY.pre_awsume(fake_app, fake_args) - self.assertEqual(fake_args.target_profile_name, 'default') - - fake_args.profile_name = 'superCoolClient' - AWSUMEPY.pre_awsume(fake_app, fake_args) - self.assertEqual(fake_args.target_profile_name, 'superCoolClient') - fake_args.profile_name = None - - fake_args.kill = True - with self.assertRaises(SystemExit): - AWSUMEPY.pre_awsume(fake_app, fake_args) - mock_kill.assert_called_once() - fake_args.kill = False - - fake_args.list_profile_names = True - with self.assertRaises(SystemExit): - AWSUMEPY.pre_awsume(fake_app, fake_args) - mock_list_profile_names.assert_called_once() - fake_args.list_profile_names = False - - fake_args.plugin_urls = ['url1', 'url2'] - with self.assertRaises(SystemExit): - AWSUMEPY.pre_awsume(fake_app, fake_args) - mock_download_plugin.assert_called_once() - fake_args.plugin_urls = None - - fake_args.delete_plugin_name = 'somePlugin' - with self.assertRaises(SystemExit): - AWSUMEPY.pre_awsume(fake_app, fake_args) - mock_delete_plugin.assert_called_once() - fake_args.delete_plugin_name = None - - fake_args.display_plugin_info = True - with self.assertRaises(SystemExit): - AWSUMEPY.pre_awsume(fake_app, fake_args) - mock_display_plugin_info.assert_called_once() - fake_args.display_plugin_info = False - - fake_args.config_help = True - with self.assertRaises(SystemExit): - AWSUMEPY.pre_awsume(fake_app, fake_args) - mock_config_help.assert_called_once() - fake_args.config_help = False - - fake_args.config = ['option', 'value'] - with self.assertRaises(SystemExit): - AWSUMEPY.pre_awsume(fake_app, fake_args) - fake_app.set_option.assert_called_once() - fake_args.config_help = None - - @mock.patch('boto3.client') - def test_create_sts_client(self, mock_client): - """test create_sts_client awsumepy function""" - AWSUMEPY.create_sts_client() - self.assertTrue('sts' in args for args in mock_client.call_args_list) - - @mock.patch('awsumepy.fix_session_credentials') - @mock.patch('awsumepy.read_mfa') - @mock.patch('awsumepy.requires_mfa') - @mock.patch('awsumepy.create_sts_client') - @mock.patch('awsumepy.is_role') - @mock.patch('awsumepy.read_aws_cache') - @mock.patch('awsumepy.write_aws_cache') - @mock.patch('awsumepy.valid_cache_session') - def test_get_user_session(self, - mock_valid_cache_session, - mock_write_cache, - mock_read_aws_cache, - mock_is_role, - mock_create_sts_client, - mock_requires_mfa, - mock_read_mfa, - mock_fix_session_credentials): - """test get_user_session awsumepy function""" - mock_get_session_token = mock.Mock() - mock_get_session_token.return_value = {'Credentials': {'SessionToken':'EXAMPLE'}} - mock_client = mock.Mock() - mock_client.get_session_token = mock_get_session_token - mock_create_sts_client.return_value = mock_client - fake_app = mock.Mock() - fake_args = mock.Mock() - fake_profiles = { - 'fake-nomfa-user': { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE' - }, - 'fake-mfa-profile': { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - 'mfa_serial': 'EXAMPLE' - }, - 'fake-role-profile': { - 'source_profile': 'fake-mfa-profile', - 'role_arn': 'EXAMPLE', - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - 'mfa_serial': 'EXAMPLE' - } - } - - fake_args.target_profile_name = 'fake-nomfa-user' - mock_requires_mfa.return_value = False - mock_is_role.return_value = False - fake_args.force_refresh = False - fake_session = AWSUMEPY.get_user_session(fake_app, fake_args, fake_profiles, '/cache/path', None) - self.assertEqual(fake_session, { - 'AccessKeyId' : 'EXAMPLE', - 'SecretAccessKey' : 'EXAMPLE', - 'region' : None, - }) - - fake_args.target_profile_name = 'fake-mfa-profile' - mock_requires_mfa.return_value = True - mock_is_role.return_value = False - fake_args.force_refresh = False - mock_valid_cache_session.return_value = True - mock_read_aws_cache.return_value = 'fake_session' - fake_session = AWSUMEPY.get_user_session(fake_app, fake_args, fake_profiles, '/cache/path', None) - self.assertEqual(fake_session, 'fake_session') - - fake_args.target_profile_name = 'fake-mfa-profile' - mock_requires_mfa.return_value = True - mock_is_role.return_value = False - fake_args.force_refresh = True - mock_valid_cache_session.return_value = False - fake_session = AWSUMEPY.get_user_session(fake_app, fake_args, fake_profiles, '/cache/path', None) - self.assertEqual(fake_session, { - 'SessionToken':'EXAMPLE' - }) - - fake_args.target_profile_name = 'fake-role-profile' - mock_requires_mfa.return_value = False - mock_is_role.return_value = True - fake_args.force_refresh = True - mock_valid_cache_session.return_value = False - fake_session = AWSUMEPY.get_user_session(fake_app, fake_args, fake_profiles, '/cache/path', None) - self.assertEqual(fake_session, { - 'SessionToken':'EXAMPLE' - }) - - @mock.patch('awsumepy.read_mfa') - @mock.patch('awsumepy.requires_mfa') - @mock.patch('awsumepy.fix_session_credentials') - @mock.patch('awsumepy.create_sts_client') - def test_get_role_session(self, - mock_create_sts_client, - mock_fix_session_credentials, - mock_requires_mfa, - mock_read_mfa): - """test get_role_session awsumepy function""" - fake_app = mock.Mock() - fake_args = mock.Mock() - fake_args.session_name = None - fake_args.target_profile_name = 'default' - fake_args.target_role_duration = 0 - mock_assume_role = mock.Mock() - mock_client = mock.Mock() - mock_client.assume_role = mock_assume_role - mock_create_sts_client.return_value = mock_client - mock_requires_mfa.return_value = False - mock_read_mfa.return_value = '123456' - fake_profiles = { - 'fake-nomfa-user': { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE' - }, - 'fake-mfa-profile': { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - 'mfa_serial': 'EXAMPLE' - }, - 'fake-role-profile': { - 'source_profile': 'fake-mfa-profile', - 'role_arn': 'EXAMPLE', - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - 'mfa_serial': 'EXAMPLE' - } - } - fake_user_session = { - 'AccessKeyId':'EXAMPLE', - 'SecretAccessKey':'EXAMPLE', - 'SessionToken':'EXAMPLE' - } - - fake_args.target_profile_name = 'fake-role-profile' - mock_assume_role.return_value = { - 'Credentials': { - 'SessionToken':'EXAMPLE' - } - } - session = AWSUMEPY.get_role_session(fake_app, fake_args, fake_profiles, fake_user_session, None) - self.assertEqual(session, {'SessionToken':'EXAMPLE'}) - mock_assume_role.assert_called_with(RoleArn='EXAMPLE', RoleSessionName='awsume-session-fake-role-profile') - - fake_args.session_name = 'cool-session' - session = AWSUMEPY.get_role_session(fake_app, fake_args, fake_profiles, fake_user_session, None) - self.assertEqual(session, {'SessionToken':'EXAMPLE'}) - mock_assume_role.assert_called_with(RoleArn='EXAMPLE', RoleSessionName='cool-session') - - fake_args.target_role_duration = 43200 - mock_requires_mfa.return_value = True - session = AWSUMEPY.get_role_session(fake_app, fake_args, fake_profiles, fake_user_session, None) - expected_call = mock.call(RoleArn='EXAMPLE', - RoleSessionName='cool-session', - DurationSeconds=43200, - SerialNumber='EXAMPLE', - TokenCode='123456') - self.assertTrue(expected_call in mock_assume_role.call_args_list) - mock_requires_mfa.return_value = False - fake_args.target_role_duration = 0 - - fake_args.target_role_duration = 43200 - session = AWSUMEPY.get_role_session(fake_app, fake_args, fake_profiles, fake_user_session, None) - expected_call = mock.call(RoleArn='EXAMPLE', - RoleSessionName='cool-session', - DurationSeconds=43200) - self.assertTrue(expected_call in mock_assume_role.call_args_list) - fake_args.target_role_duration = 0 - - - -# -# TestAutoAwsume -# -class TestAutoAwsume(unittest.TestCase): - """Test suite for autoawsume""" - @mock.patch('awsumepy.kill_all_auto_processes') - @mock.patch('awsumepy.write_auto_awsume_session') - @mock.patch('awsumepy.create_auto_profile') - def test_start_auto_awsume(self, - mock_create_auto_profile, - mock_write_auto_awsume_session, - mock_kill_all_auto_processes): - """test start_auto_awsume awsumepy function""" - fake_args = mock.Mock() - fake_app = mock.Mock() - fake_app.set_export_data = mock.Mock() - fake_user_session = {} - fake_role_session = { - 'region': 'us-east-1' - } - fake_profiles = { - 'fake-nomfa-user': { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - }, - 'fake-mfa-profile': { - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - 'mfa_serial': 'EXAMPLE' - }, - 'fake-role-profile': { - 'source_profile': 'fake-mfa-profile', - 'role_arn': 'EXAMPLE', - 'aws_access_key_id': 'EXAMPLE', - 'aws_secret_access_key': 'EXAMPLE', - 'mfa_serial': 'EXAMPLE' - } - } - - fake_args.target_profile_name = 'fake-role-profile' - fake_args.session_name = None - AWSUMEPY.start_auto_awsume(fake_args, fake_app, fake_profiles, '/creds/path', fake_user_session, fake_role_session) - fake_app.set_export_data.assert_called_with({'AWSUME_FLAG':'Auto', 'AWSUME_LIST':[ - 'auto-refresh-fake-role-profile', - 'us-east-1', - 'fake-role-profile' - ]}) - - fake_args.target_profile_name = 'fake-role-profile' - fake_args.session_name = 'custom-session-name' - AWSUMEPY.start_auto_awsume(fake_args, fake_app, fake_profiles, '/creds/path', fake_user_session, fake_role_session) - self.assertTrue(mock.call({'region':'us-east-1'}, {}, 'custom-session-name', 'fake-mfa-profile', 'EXAMPLE') in mock_create_auto_profile.call_args_list) - - @mock.patch('six.moves.configparser.ConfigParser') - def test_is_auto_profiles(self, - mock_config_parser): - """test is_auto_profiles awsumepy function""" - mock_config_object = mock.Mock() - mock_config_parser.return_value = mock_config_object - mock_config_object.read = mock.Mock() - mock_config_object.sections = mock.Mock() - mock_config_object.sections.return_value = ['auto-refresh-profile'] - self.assertTrue(AWSUMEPY.is_auto_profiles('/cred/path/')) - mock_config_object.sections.return_value = ['profile'] - self.assertFalse(AWSUMEPY.is_auto_profiles('/cred/path/')) - - @mock.patch('six.moves.builtins.open') - @mock.patch('six.moves.configparser.ConfigParser') - def test_remove_auto_profile(self, - mock_config_parser, - mock_open): - """test remove_auto_profile awsumepy function""" - mock_config_object = mock.Mock() - mock_config_parser.return_value = mock_config_object - mock_config_object.read = mock.Mock() - mock_config_object.write = mock.Mock() - mock_config_object.sections = mock.Mock() - mock_config_object.has_section = mock.Mock() - mock_config_object.remove_section = mock.Mock() - - profile_name = 'some-profile' - mock_config_object.has_section.return_value = False - AWSUMEPY.remove_auto_profile(profile_name) - mock_config_object.remove_section.assert_not_called() - - mock_config_object.has_section.return_value = True - mock_config_object.sections.return_value = [ - 'auto-refresh-profile-1', - 'auto-refresh-profile-2', - 'normal-profile-3', - ] - AWSUMEPY.remove_auto_profile(profile_name) - mock_config_object.remove_section.assert_called() - - profile_name = None - AWSUMEPY.remove_auto_profile(profile_name) - self.assertEqual(mock_config_object.remove_section.call_count, 3) - - @mock.patch('six.moves.builtins.open') - @mock.patch('six.moves.configparser.ConfigParser') - def test_write_auto_awsume_session(self, - mock_config_parser, - mock_open): - """test write_auto_awsume_session awsumepy function""" - mock_config_object = mock.Mock() - mock_config_object.read = mock.Mock() - mock_config_object.write = mock.Mock() - mock_config_object.has_section = mock.Mock() - mock_config_object.remove_section = mock.Mock() - mock_config_object.add_section = mock.Mock() - mock_config_parser.return_value = mock_config_object - mock_config_object.sections = mock.Mock() - - mock_config_object.sections.return_value = [ - 'auto-refresh-profile-1', - 'auto-refresh-profile-2', - 'normal-profile-3', - ] - profile_name = 'client-dev' - fake_auto_profile = { - 'key': 'value' - } - mock_config_object.has_section.return_value = True - AWSUMEPY.write_auto_awsume_session(profile_name, fake_auto_profile, '/cred/path') - mock_config_object.read.assert_called() - mock_config_object.remove_section.assert_called() - mock_config_object.write.assert_called() - - def test_create_auto_profile(self): - """test create_auto_profile awsumepy function""" - fake_role_session = { - 'AccessKeyId':'EXAMPLE', - 'SecretAccessKey':'EXAMPLE', - 'SessionToken':'EXAMPLE', - 'region':'EXAMPLE', - 'Expiration':'EXAMPLE', - } - fake_user_session = { - 'Expiration':'EXAMPLE' - } - fake_session_name = 'cool-session' - fake_source_profile_name = 'client-source' - fake_role_arn = 'EXAMPLE' - auto_profile = AWSUMEPY.create_auto_profile(fake_role_session, fake_user_session, fake_session_name, fake_source_profile_name, fake_role_arn) - self.assertEqual(auto_profile, { - 'aws_access_key_id' : 'EXAMPLE', - 'aws_secret_access_key' : 'EXAMPLE', - 'aws_session_token' : 'EXAMPLE', - 'aws_region' : 'EXAMPLE', - 'awsume_role_expiration' : 'EXAMPLE', - 'awsume_user_expiration' : 'EXAMPLE', - 'awsume_session_name' : 'cool-session', - 'awsume_cache_name' : 'awsume-credentials-client-source', - 'aws_role_arn' : 'EXAMPLE' - }) - - @mock.patch('psutil.process_iter') - def test_kill_all_auto_processes(self, mock_process_iter): - """test kill_all_auto_processes awsumepy function""" - proc1 = mock.Mock() - proc1.kill = mock.Mock() - proc1.cmdline = mock.Mock() - proc1.cmdline.return_value = ['bash'] - proc2 = mock.Mock() - proc2.kill = mock.Mock() - proc2.cmdline = mock.Mock() - proc2.cmdline.return_value = ['autoawsume'] - proc3 = mock.Mock() - proc3.kill = mock.Mock() - proc3.cmdline = mock.Mock() - proc3.cmdline.return_value = ['otherProgram'] - mock_process_iter.return_value = [proc1, proc2, proc3] - - AWSUMEPY.kill_all_auto_processes() - proc1.kill.assert_not_called() - proc2.kill.assert_called() - proc3.kill.assert_not_called() - - proc2.kill.side_effect = Exception - AWSUMEPY.kill_all_auto_processes() - - @mock.patch('awsumepy.kill_all_auto_processes') - @mock.patch('awsumepy.is_auto_profiles') - @mock.patch('awsumepy.remove_auto_profile') - def test_kill(self, - mock_remove_auto_profile, - mock_is_auto_profiles, - mock_kill_all_auto_processes): - """Test kill awsumepy function""" - fake_args = mock.Mock() - fake_app = mock.Mock() - fake_app.set_export_data = mock.Mock() - fake_app.export_data = mock.Mock() - - fake_args.profile_name = None - AWSUMEPY.kill(fake_args, fake_app) - mock_kill_all_auto_processes.assert_called_once() - mock_remove_auto_profile.assert_called_once_with() - fake_app.set_export_data.assert_called_once() - fake_app.export_data.assert_called_once() - - fake_args.profile_name = 'some-profile' - mock_is_auto_profiles.return_value = True - AWSUMEPY.kill(fake_args, fake_app) - self.assertEqual(mock_kill_all_auto_processes.call_count, 1) - mock_remove_auto_profile.assert_called_with('some-profile') - self.assertEqual(fake_app.set_export_data.call_count, 2) - self.assertEqual(fake_app.export_data.call_count, 2) - - fake_args.profile_name = 'some-profile' - mock_is_auto_profiles.return_value = False - AWSUMEPY.kill(fake_args, fake_app) - self.assertEqual(mock_kill_all_auto_processes.call_count, 2) - mock_remove_auto_profile.assert_called_with('some-profile') - self.assertEqual(fake_app.set_export_data.call_count, 3) - self.assertEqual(fake_app.export_data.call_count, 3) - - - -# -# TestPluginManagement -# -class TestPluginManagement(unittest.TestCase): - """Test suite for Plugin Manaement""" - def test_get_main_content_type(self): - python2_http_message_object = mock.Mock(spec=[]) - python2_http_message_object.getmaintype = mock.Mock() - python2_http_message_object.getmaintype.return_value = 'Python2MainType' - python3_http_message_object = mock.Mock(spec=[]) - python3_http_message_object.getmaintype = mock.Mock() - python3_http_message_object.getmaintype.return_value = 'Python3MainType' - - python2_type = AWSUMEPY.get_main_content_type(python2_http_message_object) - self.assertEqual(python2_type, 'Python2MainType') - python3_type = AWSUMEPY.get_main_content_type(python3_http_message_object) - self.assertEqual(python3_type, 'Python3MainType') - - @mock.patch('awsumepy.get_main_content_type') - @mock.patch('six.moves.urllib.request.urlopen') - @mock.patch('six.moves.configparser.ConfigParser') - def test_download_file(self, mock_config_parser, mock_urlopen, mock_get_content_type): - """Test download_file awsumepy function""" - mock_response = mock.Mock() - mock_response.info = mock.Mock() - mock_response.read = mock.Mock() - mock_response.read.return_value = b'plugin file contents' - mock_urlopen.return_value = mock_response - - mock_get_content_type.return_value = 'text' - fake_file = AWSUMEPY.download_file('fake-url') - self.assertEqual(fake_file, 'plugin file contents') - - mock_get_content_type.return_value = 'video' - with self.assertRaises(Exception): - fake_file = AWSUMEPY.download_file('fake-url') - - @mock.patch('six.moves.builtins.open') - @mock.patch('awsumepy.get_input') - @mock.patch('os.path.isfile') - def test_write_plugin_files(self, - mock_isfile, - mock_input, - mock_open): - """Test write_plugin_files awsumepy function""" - fake_file1 = 'python file contents' - fake_file2 = 'yapsy-plugin file contents' - fake_filename1 = 'plugin.py' - fake_filename2 = 'plugin.yapsy-plugin' - - mock_isfile.return_value = True - mock_input.return_value = 'n' - AWSUMEPY.write_plugin_files(fake_file1, fake_file2, fake_filename1, fake_filename2) - mock_open.assert_not_called() - - mock_isfile.return_value = True - mock_input.return_value = 'y' - AWSUMEPY.write_plugin_files(fake_file1, fake_file2, fake_filename1, fake_filename2) - self.assertEqual(mock_open.call_count, 2) - - mock_isfile.return_value = False - mock_input.return_value = '' - AWSUMEPY.write_plugin_files(fake_file1, fake_file2, fake_filename1, fake_filename2) - self.assertEqual(mock_open.call_count, 4) - - @mock.patch('awsumepy.read_plugin_cache') - @mock.patch('awsumepy.cache_urls') - @mock.patch('awsumepy.write_plugin_files') - @mock.patch('awsumepy.download_file') - def test_download_plugin(self, - mock_download_file, - mock_write_plugin_files, - mock_cache_urls, - mock_read_plugin_cache): - """Test download_plugin awsumepy function""" - fake_url1 = 'https://website.com/invalid/' - fake_url2 = 'https://website.com/invalid/' - AWSUMEPY.download_plugin(fake_url1, fake_url2) - mock_download_file.assert_not_called() - mock_write_plugin_files.assert_not_called() - - fake_url1 = 'https://website.com/invalid/wrongFile.txt' - fake_url2 = 'https://website.com/invalid/wrongFile.jpg' - AWSUMEPY.download_plugin(fake_url1, fake_url2) - mock_download_file.assert_not_called() - mock_write_plugin_files.assert_not_called() - - fake_url1 = 'https://website.com/valid/plugin.py' - fake_url2 = 'https://website.com/valid/plugin.yapsy-plugin' - AWSUMEPY.download_plugin(fake_url1, fake_url2) - self.assertEqual(mock_download_file.call_count, 2) - mock_write_plugin_files.assert_called() - - fake_url1 = 'plugin.py' - fake_url2 = 'plugin.yapsy-plugin' - mock_read_plugin_cache.return_value = { - 'plugin.py':'url1', - 'plugin.yapsy-plugin':'url2' - } - AWSUMEPY.download_plugin(fake_url1, fake_url2) - self.assertEqual(mock_download_file.call_count, 4) - mock_read_plugin_cache.assert_called() - self.assertEqual(mock_write_plugin_files.call_count, 2) - - mock_download_file.side_effect = Exception - AWSUMEPY.download_plugin(fake_url1, fake_url2) - self.assertEqual(mock_write_plugin_files.call_count, 2) - - @mock.patch('awsumepy.get_input') - @mock.patch('os.path.isdir') - @mock.patch('os.path.isfile') - @mock.patch('os.path.join') - @mock.patch('shutil.rmtree') - @mock.patch('os.remove') - @mock.patch('os.listdir') - def test_delete_plugin(self, - mock_ls, - mock_rm, - mock_rmtree, - mock_join, - mock_isfile, - mock_isdir, - mock_input): - """Test delete_plugin awsumepy function""" - fake_plugin_name = 'somePlugin' - mock_ls.return_value = [ - 'otherPlugin.py', - 'otherPlugin.yapsy-plugin', - ] - AWSUMEPY.delete_plugin(fake_plugin_name) - mock_rm.assert_not_called() - mock_rmtree.assert_not_called() - - mock_ls.return_value = [ - 'somePlugin.py', - 'somePlugin.yapsy-plugin', - 'otherPlugin.py', - 'otherPlugin.yapsy-plugin', - ] - mock_input.return_value = 'n' - AWSUMEPY.delete_plugin(fake_plugin_name) - mock_rm.assert_not_called() - mock_rmtree.assert_not_called() - - mock_isfile.side_effect = [True, False] - mock_isdir.side_effect = [True] - mock_input.return_value = 'y' - AWSUMEPY.delete_plugin(fake_plugin_name) - mock_rm.assert_called_once() - mock_rmtree.assert_called_once() - - @mock.patch('six.moves.builtins.open') - @mock.patch('json.load') - @mock.patch('os.path.isfile') - def test_read_plugin_cache(self, mock_isfile, mock_load, mock_open): - """Test read_plugin_cache awsumepy function""" - mock_isfile.return_value = True - mock_load.return_value = {'plugin.py':'url'} - cache = AWSUMEPY.read_plugin_cache() - self.assertEqual(cache, {'plugin.py':'url'}) - mock_open.assert_called() - - mock_isfile.return_value = False - self.assertEqual(AWSUMEPY.read_plugin_cache(), {}) - - @mock.patch('six.moves.builtins.open') - @mock.patch('json.dump') - @mock.patch('awsumepy.read_plugin_cache') - def test_cache_urls(self, mock_read_plugin_cache, mock_dump, mock_open): - """Test cache_urls awsumepy function""" - mock_read_plugin_cache.return_value = { - 'plugin.py':'url', - 'plugin.yapsy-plugin':'url', - } - AWSUMEPY.cache_urls('url', 'url', 'otherPlugin.py', 'otherPlugin.yapsy-plugin') - target_cache = { - 'plugin.py':'url', - 'plugin.yapsy-plugin':'url', - 'otherPlugin.py':'url', - 'otherPlugin.yapsy-plugin':'url', - } - args, _ = mock_dump.call_args_list[0] - self.assertTrue(target_cache in args) - - - -# -# TestAwsumeApp -# -class TestAwsumeApp(unittest.TestCase): - """Test suite for AwsumeApp""" - @mock.patch('awsumepy.PluginManager') - def test_create_plugin_manager(self, mock_plugin_manager): - """test create_plugin_manager awsumepy function""" - mock_manager = mock.Mock() - mock_manager.setPluginPlaces = mock.Mock() - mock_manager.locatePlugins = mock.Mock() - mock_manager.loadPlugins = mock.Mock() - mock_plugin_manager.PluginManager = mock.Mock() - mock_plugin_manager.PluginManager.return_value = mock_manager - - plugin1_err1 = 'we dont care' - plugin1_err2 = ModuleNotFoundError() - plugin1_err2.name = 'some_module' - plugin1_err3 = 'we dont care' - plugin1_err = (plugin1_err1, plugin1_err2, plugin1_err3) - mock_plugin_info1 = mock.Mock() - mock_plugin_info1.name = 'My plugin' - mock_plugin_info1.error = plugin1_err - mock_processed_plugins = [mock_plugin_info1] - mock_manager.loadPlugins.return_value = mock_processed_plugins - - manager = AWSUMEPY.create_plugin_manager('/dir') - mock_manager.setPluginPlaces.assert_called_once_with(['/dir']) - mock_manager.loadPlugins.assert_called_once() - self.assertEqual(manager, mock_manager) - - def test_register_plugins(self): - """test register_plugins awsumepy function""" - AWSUMEPY.__version__ = '0.0.0' - - fake_app = mock.Mock() - fake_app.register_function = mock.Mock() - fake_app.validFunctions = ['fn1', 'fn2'] - - plugin1 = mock.Mock() - plugin1.name = 'plugin1' - plugin1.plugin_object = mock.Mock() - plugin1.plugin_object.TARGET_VERSION = '0.0.0' - plugin1.plugin_object.fn1 = mock.Mock() - plugin1.plugin_object.fn2 = mock.Mock() - - plugin2 = mock.Mock() - plugin2.name = 'plugin2' - plugin2.plugin_object = mock.Mock() - plugin2.plugin_object.TARGET_VERSION = '0.0.0' - plugin2.plugin_object.fn1 = mock.Mock() - plugin2.plugin_object.fn2 = mock.Mock() - - fake_manager = mock.Mock() - fake_manager.getAllPlugins = mock.Mock() - fake_manager.getAllPlugins.return_value = [plugin1, plugin2] - - AWSUMEPY.register_plugins(fake_app, fake_manager) - self.assertEqual(fake_app.register_function.call_count, 4) - - AWSUMEPY.__version__ = '1.0.0' - AWSUMEPY.register_plugins(fake_app, fake_manager) - self.assertEqual(fake_app.register_function.call_count, 8) - - plugin2.plugin_object.TARGET_VERSION = mock.Mock(spec=[]) - AWSUMEPY.register_plugins(fake_app, fake_manager) - self.assertEqual(fake_app.register_function.call_count, 12) - - plugin2.plugin_object.TARGET_VERSION = '0.0.0' - fake_app.register_function.return_value = False - AWSUMEPY.register_plugins(fake_app, fake_manager) - self.assertEqual(fake_app.register_function.call_count, 16) - - -if __name__ == '__main__': - unittest.main() diff --git a/awsume_autocomplete.py b/awsume_autocomplete.py new file mode 100644 index 0000000..cf43ead --- /dev/null +++ b/awsume_autocomplete.py @@ -0,0 +1,41 @@ +import os +import sys +import json +import configparser +from pathlib import Path + + +def get_aws_files() -> tuple: + config_file = os.environ.get('AWS_CONFIG_FILE') if os.environ.get('AWS_CONFIG_FILE') else '~/.aws/config' + credentials_file = os.environ.get('AWS_CREDENTIALS_FILE') if os.environ.get('AWS_CREDENTIALS_FILE') else '~/.aws/credentials' + return str(Path(config_file).expanduser()), str(Path(credentials_file).expanduser()) + + +def get_profile_names(credentials_file: str, config_file: str) -> list: + credentials = configparser.ConfigParser() + credentials.read(credentials_file) + profiles = list(credentials._sections.keys()) + + config = configparser.ConfigParser() + config.read(config_file) + config_profiles = [_.replace('profile ', '') for _ in list(config._sections.keys())] + + return uniquely_concat_lists(profiles, config_profiles) + + +def uniquely_concat_lists(list1, list2): + for element in list2: + if element not in list1: + list1.append(element) + return list1 + + +def main(): + config, credentials = get_aws_files() + profile_names = get_profile_names(credentials, config) + autocomplete = json.load(open(str(Path('~/.awsume/autocomplete.json').expanduser()))) + profile_names = uniquely_concat_lists(profile_names, autocomplete['profile-names']) + print('\n'.join(profile_names)) + +if __name__ == "__main__": + main() diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js new file mode 100644 index 0000000..e93f6b8 --- /dev/null +++ b/docs/.vuepress/config.js @@ -0,0 +1,53 @@ +module.exports = { + title: 'AWSume', + description: 'Awsume - A cli that makes using AWS IAM credentials easy', + base: '/', + themeConfig: { + lastUpdated: 'Last Updated', // string | boolean + }, + themeConfig: { + displayAllHeaders: true, + // sidebarDepth: 1, + logo: '/logo.png', + nav: [ + { text: 'Home', link: '/' }, + { text: 'GitHub', link: 'https://github.com/trek10inc/awsume' }, + { text: 'Trek10', link: 'https://trek10.com' }, + ], + sidebar: [ + ['/', 'AWSume'], + { + title: 'General', + collapsable: true, + children: [ + '/general/quickstart', + '/general/aws-file-configuration', + '/general/usage', + '/general/overview', + ], + }, + { + title: 'Utilities', + collapsable: true, + children: [ + '/utilities/awsume-configure', + ], + }, + { + title: 'Advanced', + collapsable: true, + children: [ + '/advanced/autoawsume', + ], + }, + // { + // title: 'Plugins', + // collapsable: true, + // children: [ + // '/plugins/', + // '/plugins/profiles', + // ], + // }, + ], + }, +}; diff --git a/docs/.vuepress/public/cover.png b/docs/.vuepress/public/cover.png new file mode 100644 index 0000000..720ba9c Binary files /dev/null and b/docs/.vuepress/public/cover.png differ diff --git a/docs/.vuepress/public/demo.gif b/docs/.vuepress/public/demo.gif new file mode 100644 index 0000000..2662483 Binary files /dev/null and b/docs/.vuepress/public/demo.gif differ diff --git a/docs/.vuepress/public/demo.yml b/docs/.vuepress/public/demo.yml new file mode 100644 index 0000000..ce906e1 --- /dev/null +++ b/docs/.vuepress/public/demo.yml @@ -0,0 +1,183 @@ +# The configurations that used for the recording, feel free to edit them +config: + + # Specify a command to be executed + # like `/bin/bash -l`, `ls`, or any other commands + # the default is bash for Linux + # or powershell.exe for Windows + command: bash -l + + # Specify the current working directory path + # the default is the current working directory path + cwd: /Users/mbarney/dev/trek10/github/awsume/rewrite/docs + + # Export additional ENV variables + env: + recording: true + + # Explicitly set the number of columns + # or use `auto` to take the current + # number of columns of your shell + cols: 60 + + # Explicitly set the number of rows + # or use `auto` to take the current + # number of rows of your shell + rows: 10 + + # Amount of times to repeat GIF + # If value is -1, play once + # If value is 0, loop indefinitely + # If value is a positive number, loop n times + repeat: -1 + + # Quality + # 1 - 100 + quality: 100 + + # Delay between frames in ms + # If the value is `auto` use the actual recording delays + frameDelay: auto + + # Maximum delay between frames in ms + # Ignored if the `frameDelay` isn't set to `auto` + # Set to `auto` to prevent limiting the max idle time + maxIdleTime: 2000 + + # The surrounding frame box + # The `type` can be null, window, floating, or solid` + # To hide the title use the value null + # Don't forget to add a backgroundColor style with a null as type + frameBox: + type: floating + title: Awsume + style: + border: 0px black solid + # boxShadow: none + # margin: 0px + + # Add a watermark image to the rendered gif + # You need to specify an absolute path for + # the image on your machine or a URL, and you can also + # add your own CSS styles + watermark: + imagePath: null + style: + position: absolute + right: 15px + bottom: 15px + width: 100px + opacity: 0.9 + + # Cursor style can be one of + # `block`, `underline`, or `bar` + cursorStyle: block + + # Font family + # You can use any font that is installed on your machine + # in CSS-like syntax + fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" + + # The size of the font + fontSize: 12 + + # The height of lines + lineHeight: 1 + + # The spacing between letters + letterSpacing: 0 + + # Theme + theme: + background: "transparent" + foreground: "#afafaf" + cursor: "#c7c7c7" + black: "#232628" + red: "#fc4384" + green: "#b3e33b" + yellow: "#ffa727" + blue: "#75dff2" + magenta: "#ae89fe" + cyan: "#708387" + white: "#d5d5d0" + brightBlack: "#626566" + brightRed: "#ff7fac" + brightGreen: "#c8ed71" + brightYellow: "#ebdf86" + brightBlue: "#75dff2" + brightMagenta: "#ae89fe" + brightCyan: "#b1c6ca" + brightWhite: "#f9f9f4" + +# Records, feel free to edit them +records: + - delay: 1509 + content: "\e[?1034h\e[0;33m➜\e[0m\e[0;36m 18:14:18 \e[0m\e[0m\e[0m\e[0;36m ~/dev/trek10/github/awsume/rewrite/docs \e[0m\r\r\n$ " + - delay: 609 + content: e + - delay: 140 + content: h + - delay: 47 + content: c + - delay: 124 + content: o + - delay: 83 + content: ' ' + - delay: 140 + content: '"' + - delay: 384 + content: "\b\e[K" + - delay: 139 + content: "\b\e[K" + - delay: 127 + content: "\b\e[K" + - delay: 133 + content: "\b\e[K" + - delay: 204 + content: "\b\e[K" + - delay: 163 + content: c + - delay: 73 + content: h + - delay: 140 + content: o + - delay: 65 + content: ' ' + - delay: 161 + content: '"' + - delay: 161 + content: H + - delay: 291 + content: e + - delay: 79 + content: l + - delay: 117 + content: l + - delay: 130 + content: o + - delay: 62 + content: ' ' + - delay: 105 + content: w + - delay: 66 + content: o + - delay: 58 + content: r + - delay: 93 + content: l + - delay: 49 + content: d + - delay: 233 + content: '!' + - delay: 596 + content: '"' + - delay: 487 + content: "\b\e[K" + - delay: 110 + content: "\b\e[K" + - delay: 184 + content: '"' + - delay: 424 + content: "\r\nHello world\r\n\e[0;33m➜\e[0m\e[0;36m 18:14:24 \e[0m\e[0m\e[0m\e[0;36m ~/dev/trek10/github/awsume/rewrite/docs \e[0m\r\r\n$ " + - delay: 1346 + content: "logout\r\n" diff --git a/docs/.vuepress/public/logo.png b/docs/.vuepress/public/logo.png new file mode 100644 index 0000000..681556e Binary files /dev/null and b/docs/.vuepress/public/logo.png differ diff --git a/docs/advanced/autoawsume.md b/docs/advanced/autoawsume.md new file mode 100644 index 0000000..a0916ba --- /dev/null +++ b/docs/advanced/autoawsume.md @@ -0,0 +1,3 @@ +# Autoawsume + +Automatically refresh assume-role credentials! diff --git a/docs/general/aws-file-configuration.md b/docs/general/aws-file-configuration.md new file mode 100644 index 0000000..9af1af7 --- /dev/null +++ b/docs/general/aws-file-configuration.md @@ -0,0 +1,3 @@ +# Configuring `~/.aws/` + +You can read the official documentation for this [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). diff --git a/docs/general/overview.md b/docs/general/overview.md new file mode 100644 index 0000000..3ed8b2d --- /dev/null +++ b/docs/general/overview.md @@ -0,0 +1,25 @@ +# Overview + +## What is awsume? + +Awsume is a command-line utility for retreiving and exporting AWS credentials to your shell's environment. + +With awsume, you can get credentials for any profile located in your [config and credentials files](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html), including those that require MFA or an `assume-role` call. + +## How does it work? + +Awsume works by setting a number of environment variables in your shell. These are the credentials awsume will manage: + +- `AWSUME_PROFILE` +- `AWS_PROFILE` +- `AWS_DEFAULT_PROFILE` +- `AWS_ACCESS_KEY_ID` +- `AWS_SECRET_ACCESS_KEY` +- `AWS_SECURITY_TOKEN` +- `AWS_SESSION_TOKEN` +- `AWS_REGION` +- `AWS_DEFAULT_REGION` + +## Awsume vs Awsumepy + +Because awsume requires the ability to manage your current shell's environment variables, it must be sourced (unless you're running on Windows). On unix-like systems, a subshell cannot update a parent shell's environment variables. Unfortunately, you cannot `source` a python script, so awsume is architected with a shell wrapper (awsume) around a python script (awsumepy). diff --git a/docs/general/quickstart.md b/docs/general/quickstart.md new file mode 100644 index 0000000..f56e444 --- /dev/null +++ b/docs/general/quickstart.md @@ -0,0 +1,37 @@ +# Quick Start + +## Installation + +Awsume can be installed via the following command: + +``` +pip install awsume +``` + +::: warning +Awsume version 4 and up requires Python 3.5+ +::: + +## Alias Setup + +If you're running on a unix-like system, you must have an alias setup for awsume, that may or may not look something like this: + +```bash +alias awsume=". awsume" +``` + +Awsume will make an attempt to place this in a login script such as your `~/.bash_profile` or `~/.bashrc` when it's being installed, so you may need to restart your terminal or re-source your login file. + +Sometimes, however, things (like permissions issues) can prevent awsume from injecting the alias. If this is the case, check out the `awsume-configure` guide [here](./awsume-configure.md). + +## Quick Usage + +Once you have your alias setup, awsume can now work! + +Run the following command and you'll be able to execute commands and run scripts with that profile's credentials. + +```python +awsume +``` + +Read more about awsume's usage [here](./usage.md). diff --git a/docs/general/usage.md b/docs/general/usage.md new file mode 100644 index 0000000..61bc4ba --- /dev/null +++ b/docs/general/usage.md @@ -0,0 +1,164 @@ +# Usage + +There's quite a few things awsume can do for you. + +If you run `awsume -h` you can see a sizeable list of options (as of 4.0.0): + +```bash +usage: awsume [-h] [-v] [-r] [-s] [-u] [-a] [-k] [-l [more]] + [--refresh-autocomplete] [--role-arn role_arn] + [--source-profile source_profile] [--external-id external_id] + [--mfa-token mfa_token] [--region region] + [--session-name session_name] [--role-duration role_duration] + [--with-saml | --with-web-identity] + [--credentials-file credentials_file] [--config-file config_file] + [--config [option [option ...]]] [--info] [--debug] + [profile_name] + +Awsume - A cli that makes using AWS IAM credentials easy + +positional arguments: + profile_name The target profile name + +optional arguments: + -h, --help show this help message and exit + -v, --version Display the current version of awsume + -r, --refresh Force refresh credentials + -s, --show-commands Show the commands to set the credentials + -u, --unset Unset your aws environment variables + -a, --auto-refresh Auto refresh credentials + -k, --kill-refresher Kill autoawsume + -l [more], --list-profiles [more] List profiles, "more" for detail (slow) + --refresh-autocomplete Refresh all plugin autocomplete profiles + --role-arn role_arn Role ARN to assume + --source-profile source_profile source_profile to use (role-arn only) + --external-id external_id External ID to pass to the assume_role + --mfa-token mfa_token Your mfa token + --region region The region you want to awsume into + --session-name session_name Set a custom role session name + --role-duration role_duration Seconds to get role creds for + --with-saml Use saml (requires plugin) + --with-web-identity Use web identity (requires plugin) + --credentials-file credentials_file Target a shared credentials file + --config-file config_file Target a config file + --config [option [option ...]] Configure awsume + --info Print any info logs to stderr + --debug Print any debug logs to stderr + +Thank you for using AWSume! Check us out at https://trek10.com +``` + +## Refresh + +The `--refresh` flag will tell awsume to ignore any cached credentials and get a new session token. + +## Show Commands + +The `--show-commands` flag will display the exact commands required to export awsume's credentials to a different shell session, like this: + +``` +$ awsume my-admin -s +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +export AWS_SECURITY_TOKEN= +export AWS_REGION= +export AWS_DEFAULT_REGION= +export AWSUME_PROFILE=my-admin +``` + +This way you can easily get credentials to another shell session, for instance through ssh. + +This works on Bash, Zsh, PowerShell, and Windows Command Prompt. + +## Unset + +The `--unset` flag will clear your current shell's AWS environment variables. + +## Auto Refresh + +The `--auto-refresh` flag will tell awsume to automatically refresh the credentials. You can read more about how this works [here](../advanced/autoawsume.md) + +## Kill Refresher + +The `--kill-refresher` flag will handle stopping autoawsume from refreshing a profile. If you pass a profile name along with the flag, that profile will no longer be refreshed. If no profile name is passed along with this flag, then all auto-refreshed profiles will be stopped. + +## List Profiles + +The `--list-profiles` flag will list data on all of the profiles it has available to it (from the config and shared credentials files or any plugins). + +If you supply an additional argument "more" to this flag, you can tell awsume to get more data than what is present locally. Currently this only means making the `sts.get_caller_identity` call to get the account ID if it can't derive it from a `role_arn` or `mfa_serial`. + +## Refresh Autocomplete + +In order to keep autocomplete fast, we do not make use of any of awsumepy's modules or any slow entry points. However, this means that any plugins that supply profiles won't be able to supply autocomplete with their profile names. To circumvent this, we utilize an autocomplete file located at `~/.awsume/autocomplete.json`. When you pass the `--refresh-autocomplete` flag to awsume, it makes the calls to all plugins to collect all profile names together into that file. That way, when the `awsume-autocomplete` helper is called, it simply reads from the config and credentials files, and the `~/.awsume/autocomplete.json` file to return a list of awsume-able profile names. + +## Role ARN + +As of awsume 4, you can use the `--role-arn` flag to awsume a specific role using your current credentials. You can also use a shorthand that follows the following format: `:`. + +## Source Profile + +To help with the Role ARN flag, you can pass in a `--source-profile` flag to target a specific profile to be the source of the `assume_role` call for the given role arn. + +## External ID + +If you don't have an external ID for your role present in your config or credentials files, you can supply this through the command line with the `--external-id` flag. + +## MFA Token + +If you want to supply the mfa token through the CLI without the interactive prompt, you can supply the `--mfa-token` flag with your mfa code. + +## Region + +You can target a specific region to awsume with the `--region` flag. This basically amounts to setting the `AWS_REGION` and `AWS_DEFAULT_REGION` environment variables. Useful for overriding the region found in a config profile. + +## Session Name + +You can supply your own session name to the `assume_role` call with the `--session-name` flag. + +## Role Duration + +You can also supply a custom role duration (up to 43200) for the number of seconds to request role credentials for with the `--role-duration` flag. + +## With SAML + +The `--with-saml` flag will tell awsume to invoke any `assume_role_with_saml` plugins you have installed. There is no default implementation for this. + +## With Web Identity + +The `--with-web-identity` flag will tell awsume to invoke any `assume_role_with_web_identity` plugins you have installed. There is no default implementation for this. + +## Credentials File + +With the `--credentials-file` flag, you can target a credentials file to use, instead of the default `~/.aws/credentials` file or whatever is pointed to with the `AWS_SHARED_CREDENTIALS_FILE` environment variable. + +## Config File + +With the `--config-file` flag, you can target a config file to use, instead of the default `~/.aws/config` file or whatever is pointed to with the `AWS_CONFIG_FILE` environment variable. + +## Config + +The `--config` flag will help you configure awsume and any plugins making use of the configuration system. + +You can set a value like this: + +``` +awsume --config set role-duration 43200 +``` + +Or you can reset to the default value with: + +``` +awsume --config reset role-duration +``` + +The configuration is stored in the `~/.awsume/config.json` file. + +## Info + +The `--info` flag will display any INFO-level logs. + +## Debug + +The `--debug` flag will display any DEBUG-level logs. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..4544ef7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,9 @@ +# AWSume: AWS Assume Made Awesome! + + + +Awsume is a convenient way to manage session tokens and assume role credentials. + + + +For a quick getting started guide, check out the [quick start](./quickstart.md) section! diff --git a/docs/utilities/awsume-configure.md b/docs/utilities/awsume-configure.md new file mode 100644 index 0000000..0c72480 --- /dev/null +++ b/docs/utilities/awsume-configure.md @@ -0,0 +1,107 @@ +# `awsume-configure` + +The `awsume-configure` command is intended to let you set up awsume, in case there was a problem during the initial installation of awsume. + +## Usage + +``` +usage: awsume-configure [-h] + --shell shell + --autocomplete-file autocomplete_file + [--alias-file alias_file] + +optional arguments: + -h, --help show this help message and exit + --shell shell The shell you will use awsume under + --autocomplete-file autocomplete_file The file you want the autocomplete script to be defined in + --alias-file alias_file The file you want the alias to be defined in +``` + +--- + +Depending on your shell, there are a few things you'll need. + +## Alias + +The alias is required for unix-like systems and shells, including Bash and Zsh. It is not required for Windows-like shells such as PowerShell or Windows Command Prompt. + +The alias should be defined in your shell's login file, so it gets loaded on every shell session. It should look something like this: + +```bash +alias awsume=". awsume" +``` + +If you are using awsume through a [pyenv](https://github.com/pyenv/pyenv) environment, your alias should look like this: + +```bash +alias awsume=". $(pyenv which awsume)" +``` + +However, if you use pyenv and also have installed awsume with [pipx](https://github.com/pipxproject/pipx), you will need the original alias. + +Awsume will try to give you the correct alias depending on your installation method, but sometimes things can go awry and require manual intervention. + +## Autocomplete script + +Autocomplete is currently only supported for Bash, Zsh, and Powershell. + +### Bash + +In Bash environments, the autocomplete script should look something like this: + +```bash +_awsume() { + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts=$(awsume-autocomplete) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} +complete -F _awsume awsume +``` + +It will attempt to load this into the first file it finds available of the following login files: + +- `~/.bash_profile` +- `~/.bash_login` +- `~/.profile` +- `~/.bashrc` + +### Zsh + +In Zsh environments, the autocomplete script should look something like this: + +```zsh +#compdef awsume +_arguments "*: :($(awsume-autocomplete))" +``` + +It will attempt to load this into `$ZDOTDIR/.zshenv`. + +### PowerShell + +For PowerShell environments, awsume will execute the following command to get the file to install the autocomplete script into: + +```powershell +powershell $profile +``` + +The autocomplete script will look something like this: + +```powershell +Register-ArgumentCompleter -Native -CommandName awsume -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + $(awsume-autocomplete) | + Where-Object { $_ -like "$wordToComplete*" } | + Sort-Object | + ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} +``` + +--- + +Again, if anything went wrong during the initial installation, you can attempt to set up awsume manually with the `awsume-configure` command, passing in the locations in which to install each component. diff --git a/examplePlugin/console.md b/examplePlugin/console.md deleted file mode 100644 index e626029..0000000 --- a/examplePlugin/console.md +++ /dev/null @@ -1,44 +0,0 @@ -# AWSume Console Plugin - -This is a plugin that enables you to use your assumed role credentials to open the AWS console in your default browser. To install, just place the `awsumeConsole.py` and `awsumeConsole.yapsy-plugin` within your `~/.aws/awsumePlugins` directory, as you would install any other AWSume plugin. - -## Dependencies - -This plugin does require you to have the python module `requests` installed, which can be done with a simple command: - -```bash -pip install requests[security] -``` - -Note: the [security] specifier is not required if you have the latest version of pyopenssl installed on your system already. - -## Installation - -To install this plugin, using AWSume's latest `--install-plugin` feature, run: - -``` bash -awsume --install-plugin https://raw.githubusercontent.com/trek10inc/awsume/master/examplePlugin/console.py https://raw.githubusercontent.com/trek10inc/awsume/master/examplePlugin/console.yapsy-plugin -``` - -## Use - -There are two ways to use this plugin. - -- Use your current environment variables to open the console - - `awsume -c` Will open the AWS console using the current environment variables -- Use a profile_name - - `awsume -c` Will run AWSume on `` as it normally would, but will open the console using the credentials from running AWSume on ``. - -### Get Console Link - -- If you want to get the url itself instead of trying to open the console, use: - -``` bash -awsume -cl -``` - -or - -``` bash -awsume --console-link -``` diff --git a/examplePlugin/console.py b/examplePlugin/console.py deleted file mode 100644 index d2887a8..0000000 --- a/examplePlugin/console.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import print_function -import sys -import os -import json -import webbrowser -import urllib - -from yapsy import IPlugin -from awsume import awsumepy - -# Python 3 compatibility (python 3 has urlencode in parse sub-module) -URLENCODE = getattr(urllib, 'parse', urllib).urlencode -# Python 3 compatibility (python 3 has urlopen in parse sub-module) -URLOPEN = getattr(urllib, 'request', urllib).urlopen - -class AwsumeConsole(IPlugin.IPlugin): - """The AWS Management Console plugin. Opens an assumed-role to the AWS management console.""" - TARGET_VERSION = '3.0.0' - - def add_arguments(self, argument_parser): - """Add the console flag.""" - argument_parser.add_argument('-c', '--console', - action='store_true', - default=False, - dest='open_console', - help='Open the AWS console to the AWSume\'d credentials') - argument_parser.add_argument('-cl', '--console-link', - action='store_true', - default=False, - dest='open_console_link', - help='Show the link to open the console with the credentials') - return argument_parser - - def pre_awsume(self, app, args): - """If no profile_name is given to AWSume, check the environment for credentials.""" - #use the environment variables to open - if args.open_console_link: - args.open_console = True - if args.open_console is True and args.profile_name is None: - credentials, region = self.get_environment_credentials() - response = self.make_aws_federation_request(credentials) - signin_token = self.get_signin_token(response) - console_url = self.get_console_url(signin_token, region) - self.open_browser_to_url(console_url, args) - exit(0) - - def post_awsume(self, - app, - args, - profiles, - user_session, - role_session): - """Open the console using the currently AWSume'd credentials.""" - if args.open_console is True: - if not role_session: - awsumepy.safe_print('Cannot use these credentials to open the AWS Console.') - return - credentials, region = self.get_session_temp_credentials(role_session) - response = self.make_aws_federation_request(credentials) - signin_token = self.get_signin_token(response) - console_url = self.get_console_url(signin_token, region) - self.open_browser_to_url(console_url, args) - - def get_environment_credentials(self): - """Get session credentials from the environment.""" - aws_region = 'us-east-1' - if 'AWS_PROFILE' in os.environ: - credentials_profiles = awsumepy.read_ini_file(awsumepy.AWS_CREDENTIALS_FILE) - auto_profile = credentials_profiles[os.environ['AWS_PROFILE']] - temp_credentials = { - 'sessionId': auto_profile['aws_access_key_id'], - 'sessionKey': auto_profile['aws_secret_access_key'], - 'sessionToken': auto_profile['aws_session_token'] - } - if auto_profile.get('aws_region'): - aws_region = auto_profile.get('aws_region') - elif os.environ.get('AWS_ACCESS_KEY_ID') and os.environ.get('AWS_SECRET_ACCESS_KEY') and os.environ.get('AWS_SESSION_TOKEN'): - temp_credentials = { - 'sessionId': os.environ['AWS_ACCESS_KEY_ID'], - 'sessionKey': os.environ['AWS_SECRET_ACCESS_KEY'], - 'sessionToken': os.environ['AWS_SESSION_TOKEN'] - } - if os.environ.get('AWS_REGION'): - aws_region = os.environ['AWS_REGION'] - else: - awsumepy.safe_print('Cannot use these credentials to open the AWS Console.') - exit(0) - json_temp_credentials = json.dumps(temp_credentials) - return json_temp_credentials, aws_region - - def get_session_temp_credentials(self, session): - """Create a properly formatted json string of the given session. Return the session and the region to use.""" - if session.get('AccessKeyId') and session.get('SecretAccessKey') and session.get('SessionToken'): - aws_region = 'us-east-1' - temp_credentials = { - 'sessionId': session['AccessKeyId'], - 'sessionKey': session['SecretAccessKey'], - } - if 'SessionToken' in session: - temp_credentials['sessionToken'] = session['SessionToken'] - if session.get('region'): - aws_region = session['region'] - - #format the credentials into a json formatted string - json_temp_credentials = json.dumps(temp_credentials) - return json_temp_credentials, aws_region - awsumepy.safe_print('Cannot use these credentials to open the AWS Console.') - exit(0) - - def make_aws_federation_request(self, temp_credentials): - """Make the AWS federation request to get the signin token.""" - params = { - 'Action': 'getSigninToken', - 'Session': temp_credentials, - } - request_url = 'https://signin.aws.amazon.com/federation?' - response = URLOPEN(request_url + URLENCODE(params)) - return response - - def get_signin_token(self, aws_response): - """Get the signin token from the aws federation response.""" - raw = aws_response.read() - try: - return json.loads(raw)['SigninToken'] - except getattr(json.decoder, 'JSONDecoderError', ValueError): - # catches python3-related byte encoding - return json.loads(raw.decode())['SigninToken'] - - def get_console_url(self, aws_signin_token, aws_region): - """Get the url to open the browser to.""" - params = { - 'Action': 'login', - 'Issuer': '', - 'Destination': 'https://console.aws.amazon.com/console/home?region=' + aws_region, - 'SigninToken': aws_signin_token - } - url = 'https://signin.aws.amazon.com/federation?' - url += URLENCODE(params) - return url - - def open_browser_to_url(self, url, args): - """Open the default browser to the given url. If that fails, display the url.""" - if args.open_console_link: - awsumepy.safe_print(url) - else: - try: - webbrowser.open(url) - except Exception: - awsumepy.safe_print('Cannot open browser, here is the link:') - awsumepy.safe_print(url) diff --git a/examplePlugin/console.yapsy-plugin b/examplePlugin/console.yapsy-plugin deleted file mode 100644 index c0e6346..0000000 --- a/examplePlugin/console.yapsy-plugin +++ /dev/null @@ -1,9 +0,0 @@ -[Core] -Name = awsumeConsolePlugin -Module = console - -[Documentation] -Author = Trek10, Inc. -Version = 1.0 -Website = https://www.trek10.com/ -Description = Open the AWS console in a browser after assuming the role diff --git a/fastentrypoints.py b/fastentrypoints.py new file mode 100644 index 0000000..9707f74 --- /dev/null +++ b/fastentrypoints.py @@ -0,0 +1,112 @@ +# noqa: D300,D400 +# Copyright (c) 2016, Aaron Christianson +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +Monkey patch setuptools to write faster console_scripts with this format: + + import sys + from mymodule import entry_function + sys.exit(entry_function()) + +This is better. + +(c) 2016, Aaron Christianson +http://github.com/ninjaaron/fast-entry_points +''' +from setuptools.command import easy_install +import re +TEMPLATE = '''\ +# -*- coding: utf-8 -*- +# EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}' +__requires__ = '{3}' +import re +import sys + +from {0} import {1} + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit({2}())''' + + +@classmethod +def get_args(cls, dist, header=None): # noqa: D205,D400 + """ + Yield write_script() argument tuples for a distribution's + console_scripts and gui_scripts entry points. + """ + if header is None: + # pylint: disable=E1101 + header = cls.get_header() + spec = str(dist.as_requirement()) + for type_ in 'console', 'gui': + group = type_ + '_scripts' + for name, ep in dist.get_entry_map(group).items(): + # ensure_safe_name + if re.search(r'[\\/]', name): + raise ValueError("Path separators not allowed in script names") + script_text = TEMPLATE.format( + ep.module_name, ep.attrs[0], '.'.join(ep.attrs), + spec, group, name) + # pylint: disable=E1101 + args = cls._get_script_args(type_, name, header, script_text) + for res in args: + yield res + + +# pylint: disable=E1101 +easy_install.ScriptWriter.get_args = get_args + + +def main(): + import os + import re + import shutil + import sys + dests = sys.argv[1:] or ['.'] + filename = re.sub('\.pyc$', '.py', __file__) + + for dst in dests: + shutil.copy(filename, dst) + manifest_path = os.path.join(dst, 'MANIFEST.in') + setup_path = os.path.join(dst, 'setup.py') + + # Insert the include statement to MANIFEST.in if not present + with open(manifest_path, 'a+') as manifest: + manifest.seek(0) + manifest_content = manifest.read() + if 'include fastentrypoints.py' not in manifest_content: + manifest.write(('\n' if manifest_content else '') + + 'include fastentrypoints.py') + + # Insert the import statement to setup.py if not present + with open(setup_path, 'a+') as setup: + setup.seek(0) + setup_content = setup.read() + if 'import fastentrypoints' not in setup_content: + setup.seek(0) + setup.truncate() + setup.write('import fastentrypoints\n' + setup_content) diff --git a/package.json b/package.json deleted file mode 100644 index e6e65fc..0000000 --- a/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "awsume", - "version": "3.2.9", - "description": "Awsume - A cli that makes using AWS IAM credentials easy", - "author": { - "name":"Trek10, Inc", - "email" : "package-management@trek10.com" - }, - "scripts": { - "test": "pipenv run pytest --cov --cov-report xml:cov.xml", - "coverage": "pipenv run coverage report", - "tag": "git tag $npm_package_version && git push --tags", - "upload-prod": "python setup.py sdist upload -r pypi", - "upload-test": "python setup.py sdist upload -r pypitest", - "download-test": "pip install -i https://test.pypi.org/simple/ awsume" - }, - "license": "MIT", - "homepage": "https://github.com/trek10inc/awsume" -} diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 578ede1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README diff --git a/setup.py b/setup.py index 7352b2d..2edd962 100644 --- a/setup.py +++ b/setup.py @@ -1,205 +1,36 @@ -import atexit -import os -import subprocess -import json -from distutils.spawn import find_executable -from setuptools import setup -from setuptools.command.install import install -from setuptools.command.install_scripts import install_scripts +import fastentrypoints +from setuptools import setup, find_packages -PACKAGE = json.load(open('package.json')) -HOME_FOLDER = os.path.expanduser('~') - -BASH_AUTOCOMPLETE_SCRIPT = """ -_awsume() { - local cur prev opts - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - opts=$(awsumepy --rolesusers) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 -} -complete -F _awsume awsume -""" -ZSH_AUTOCOMPLETE_SCRIPT = """#compdef awsume -_arguments "*: :($(awsumepy --rolesusers))" -""" -POWERSHELL_AUTOCOMPLETE_SCRIPT = """ -Register-ArgumentCompleter -Native -CommandName awsume -ScriptBlock { - param($wordToComplete, $commandAst, $cursorPosition) - $(awsumepy --rolesusers) | - Where-Object { $_ -like "$wordToComplete*" } | - Sort-Object | - ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) - } -} -""" - -class CustomInstall(install): - """Run the post-install scripts to load in autocomplete, alias, and anything else for awsume.""" - def get_awsume_alias(self): - """Return the alias for awsume. - If pyenv is being used, return an alias to the current python installation's awsume.""" - awsume_alias = 'alias awsume=". awsume"' - if find_executable('pyenv'): - awsume_alias = 'alias awsume=". \$(pyenv which awsume)"' - return awsume_alias - - def install_alias(self, file_path, alias): - """Install the given aliad to the given file. - Add a comment above the alias so that users know what it's for.""" - with open(file_path, 'r') as read_f: - contents = read_f.read() - if alias not in contents: - out = open(file_path, 'a') - out.write('\n') - out.write('#AWSume alias to source the AWSume script') - out.write('\n') - out.write(alias) - out.write('\n') - out.close() - - def install_bash_script(self, file_path, script): - """Install AWSume's auto-complete to bash rc file.""" - with open(file_path, 'r') as read_f: - contents = read_f.read() - if script not in contents: - out = open(file_path, 'a') - out.write('\n') - out.write('#Auto-Complete function for AWSume') - out.write('\n') - out.write(script) - out.write('\n') - out.close() - - def install_zsh_script(self, zsh_script, rc_file, function_path): - """Install AWSume's auto-complete to zsh rc file.""" - if not os.path.exists(function_path): - os.makedirs(function_path) - - #add the directory to fpath - with open(rc_file, 'r') as original: - data = original.read() - original.close() - fpath_line = 'fpath=(' + function_path + ' $fpath)' - if fpath_line not in data: - with open(rc_file, 'a') as modified: - modified.write(fpath_line) - modified.close() - - func_file = function_path + '/_awsume' - with open(func_file, 'w+') as read_f: - content = read_f.read() - if not zsh_script in content: - out = open(func_file, 'a') - out.write(zsh_script + '\n') - out.close() - - def install_powershell_script(self, script, powershell_file): - """Install AWSume's auto-complete to powershell profile.""" - contents = open(powershell_file, 'w+').read() - if script not in contents: - out = open(powershell_file, 'a+') - out.write('\n') - out.write(script) - out.write('\n') - out.close() - - def get_bash_file(self, homefolder): - """Return the path to the user's bash rc file.""" - rc_file = os.path.abspath('%s/.bashrc' % homefolder) - if os.path.exists(os.path.abspath('%s/.bash_aliases' % homefolder)): - rc_file = os.path.abspath('%s/.bash_aliases' % homefolder) - elif os.path.exists(os.path.abspath('%s/.bashrc' % homefolder)): - rc_file = os.path.abspath('%s/.bashrc' % homefolder) - elif os.path.exists(os.path.abspath('%s/.bash_profile' % homefolder)): - rc_file = os.path.abspath('%s/.bash_profile' % homefolder) - elif os.path.exists(os.path.abspath('%s/.profile' % homefolder)): - rc_file = os.path.abspath('%s/.profile' % homefolder) - elif os.path.exists(os.path.abspath('%s/.login' % homefolder)): - rc_file = os.path.abspath('%s/.login' % homefolder) - return rc_file - - def get_zsh_file(self, homefolder): - """Return the path to the user's zsh rc file.""" - rc_file = os.path.abspath('%s/.zshrc' % homefolder) - if os.path.exists(os.path.abspath('%s/.zshrc' % homefolder)): - rc_file = os.path.abspath('%s/.zshrc' % homefolder) - elif os.path.exists(os.path.abspath('%s/.zshenv' % homefolder)): - rc_file = os.path.abspath('%s/.zshenv' % homefolder) - elif os.path.exists(os.path.abspath('%s/.zprofile' % homefolder)): - rc_file = os.path.abspath('%s/.zprofile' % homefolder) - elif os.path.exists(os.path.abspath('%s/.zlogin' % homefolder)): - rc_file = os.path.abspath('%s/.zlogin' % homefolder) - return rc_file - - def ensure_executable(self): - """Make sure AWSume is executable""" - (awsume_path, _) = subprocess.Popen(["which", "awsume"], stdout=subprocess.PIPE).communicate() - awsume_path = awsume_path.strip() - if os.path.exists(awsume_path): - os.chmod(awsume_path, int('755', 8)) - - def run(self): - def _post_install(): - """Run post-install operations""" - awsume_alias = self.get_awsume_alias() - - # install to bash - bash_rc_file = self.get_bash_file(HOME_FOLDER) - self.install_alias(bash_rc_file, awsume_alias) - self.install_bash_script(bash_rc_file, BASH_AUTOCOMPLETE_SCRIPT) - - # install to zsh - if find_executable('zsh'): - zsh_rc_file = self.get_zsh_file(HOME_FOLDER) - function_path = os.path.abspath('/usr/local/share/zsh/site-functions') - self.install_alias(zsh_rc_file, awsume_alias) - self.install_zsh_script(ZSH_AUTOCOMPLETE_SCRIPT, zsh_rc_file, function_path) - - # install to powershell - if find_executable('powershell'): - (file_name, _) = subprocess.Popen(["powershell", "$profile"], stdout=subprocess.PIPE, shell=True).communicate() - file_name = str(file_name.decode('ascii')).replace('\r\n', '') - self.install_powershell_script(POWERSHELL_AUTOCOMPLETE_SCRIPT, file_name) - - # make executable - self.ensure_executable() - atexit.register(_post_install) - install.run(self) +import awsume +from awsume.configure.post_install import CustomInstall setup( - name=PACKAGE['name'], - packages=['awsume'], - version=PACKAGE['version'], - author=PACKAGE['author']['name'], - author_email=PACKAGE['author']['email'], - description=PACKAGE['description'], - license=PACKAGE['license'], - url=PACKAGE['homepage'], - package_data={'awsume': ['package.json']}, - include_package_data=True, - install_requires=[ - 'boto3', - 'psutil', - 'yapsy', - 'future', - 'colorama', - ], + name=awsume.__NAME__, + packages=find_packages(), + version=awsume.__VERSION__, + author=awsume.__AUTHOR__, + author_email=awsume.__AUTHOR_EMAIL__, + description=awsume.__DESCRIPTION__, + long_description=open('readme.md', 'r').read(), + long_description_content_type='text/markdown', + license=awsume.__LICENSE__, + url=awsume.__HOMEPAGE__, + install_requires=[], scripts=[ - 'awsume/shellScripts/awsume', - 'awsume/shellScripts/awsume.ps1', - 'awsume/shellScripts/awsume.bat', - 'awsume/shellScripts/awsume.fish', + 'shell_scripts/awsume', + 'shell_scripts/awsume.ps1', + 'shell_scripts/awsume.bat', + 'shell_scripts/awsume.fish', ], entry_points={ - "console_scripts": [ - 'awsumepy=awsume.awsumepy:main', - 'autoawsume=awsume.autoawsume:main', - ] + 'console_scripts': [ + 'awsumepy=awsume.awsumepy.main:main', + 'autoawsume=awsume.autoawsume.main:main', + 'awsume-configure=awsume.configure.main:main', + 'awsume-autocomplete=awsume_autocomplete:main', + ], }, + python_requires='>=3.5', cmdclass={ 'install': CustomInstall, }, diff --git a/shell_scripts/awsume b/shell_scripts/awsume new file mode 100755 index 0000000..4131c63 --- /dev/null +++ b/shell_scripts/awsume @@ -0,0 +1,130 @@ +#!/bin/bash + +AWSUME_OUTPUT=($(awsumepy $@)) +AWSUME_FLAG=$(echo "${AWSUME_OUTPUT[0]}" | tr -d '\r') + +if [ "$AWSUME_FLAG" = "usage:" ]; then + awsumepy $@ + + +elif [ "$AWSUME_FLAG" = "Version" ]; then + awsumepy $@ + + +elif [ "$AWSUME_FLAG" = "Listing..." ]; then + awsumepy $@ + + +elif [ "$AWSUME_FLAG" = "Auto" ]; then + unset AWS_SECRET_ACCESS_KEY + unset AWS_SESSION_TOKEN + unset AWS_SECURITY_TOKEN + unset AWS_ACCESS_KEY_ID + unset AWS_REGION + unset AWS_DEFAULT_REGION + unset AWS_PROFILE + unset AWS_DEFAULT_PROFILE + unset AWSUME_PROFILE + export AWS_PROFILE=${AWSUME_OUTPUT[1]} + export AWS_DEFAULT_PROFILE=${AWSUME_OUTPUT[1]} + if [ ! "${AWSUME_OUTPUT[2]}" = "None" ]; then + export AWS_REGION=${AWSUME_OUTPUT[2]} + export AWS_DEFAULT_REGION=${AWSUME_OUTPUT[2]} + fi + if [ ! "${AWSUME_OUTPUT[3]}" = "None" ]; then + export AWSUME_PROFILE=${AWSUME_OUTPUT[3]} + fi + #run the background autoawsume process + autoawsume & disown + + +elif [ "$AWSUME_FLAG" = "Unset" ]; then + unset AWS_PROFILE + unset AWS_DEFAULT_PROFILE + unset AWS_SECRET_ACCESS_KEY + unset AWS_SESSION_TOKEN + unset AWS_SECURITY_TOKEN + unset AWS_ACCESS_KEY_ID + unset AWS_REGION + unset AWS_DEFAULT_REGION + unset AWSUME_PROFILE + for AWSUME_var in "$@" + do + if [[ "$AWSUME_var" == "-s"* ]]; then + echo unset AWS_PROFILE + echo unset AWS_DEFAULT_PROFILE + echo unset AWS_SECRET_ACCESS_KEY + echo unset AWS_SESSION_TOKEN + echo unset AWS_SECURITY_TOKEN + echo unset AWS_ACCESS_KEY_ID + echo unset AWS_REGION + echo unset AWS_DEFAULT_REGION + echo unset AWSUME_PROFILE + fi + done + return + + +elif [ "$AWSUME_FLAG" = "Kill" ]; then + unset AWS_PROFILE + unset AWS_DEFAULT_PROFILE + unset AWS_SECRET_ACCESS_KEY + unset AWS_SESSION_TOKEN + unset AWS_SECURITY_TOKEN + unset AWS_ACCESS_KEY_ID + unset AWS_REGION + unset AWS_DEFAULT_REGION + unset AWSUME_PROFILE + return + + +elif [ "$AWSUME_FLAG" = "Stop" ]; then + if [ "auto-refresh-${AWSUME_OUTPUT[1]}" == "$AWS_PROFILE" ]; then + unset AWS_PROFILE + unset AWS_DEFAULT_PROFILE + fi + return + + +elif [ "$AWSUME_FLAG" = "Awsume" ]; then + unset AWS_SECRET_ACCESS_KEY + unset AWS_SESSION_TOKEN + unset AWS_SECURITY_TOKEN + unset AWS_ACCESS_KEY_ID + unset AWS_REGION + unset AWS_DEFAULT_REGION + unset AWS_PROFILE + unset AWS_DEFAULT_PROFILE + unset AWSUME_PROFILE + export AWS_ACCESS_KEY_ID=${AWSUME_OUTPUT[1]} + export AWS_SECRET_ACCESS_KEY=${AWSUME_OUTPUT[2]} + if [ ! "${AWSUME_OUTPUT[3]}" = "None" ]; then + export AWS_SESSION_TOKEN=${AWSUME_OUTPUT[3]} + export AWS_SECURITY_TOKEN=${AWSUME_OUTPUT[3]} + fi + if [ ! "${AWSUME_OUTPUT[4]}" = "None" ]; then + export AWS_REGION=${AWSUME_OUTPUT[4]} + export AWS_DEFAULT_REGION=${AWSUME_OUTPUT[4]} + fi + if [ ! "${AWSUME_OUTPUT[5]}" = "None" ]; then + export AWSUME_PROFILE=${AWSUME_OUTPUT[5]} + fi + for AWSUME_var in "$@" + do + if [[ "$AWSUME_var" == "-s"* ]]; then + echo export AWS_ACCESS_KEY_ID=${AWSUME_OUTPUT[1]} + echo export AWS_SECRET_ACCESS_KEY=${AWSUME_OUTPUT[2]} + if [ ! "${AWSUME_OUTPUT[3]}" = "None" ]; then + echo export AWS_SESSION_TOKEN=${AWSUME_OUTPUT[3]} + echo export AWS_SECURITY_TOKEN=${AWSUME_OUTPUT[3]} + fi + if [ ! "${AWSUME_OUTPUT[4]}" = "None" ]; then + echo export AWS_REGION=${AWSUME_OUTPUT[4]} + echo export AWS_DEFAULT_REGION=${AWSUME_OUTPUT[4]} + fi + if [ ! "${AWSUME_OUTPUT[5]}" = "None" ]; then + echo export AWSUME_PROFILE=${AWSUME_OUTPUT[5]} + fi + fi + done +fi diff --git a/awsume/shellScripts/awsume.bat b/shell_scripts/awsume.bat similarity index 88% rename from awsume/shellScripts/awsume.bat rename to shell_scripts/awsume.bat index df0ba15..db2220d 100644 --- a/awsume/shellScripts/awsume.bat +++ b/shell_scripts/awsume.bat @@ -2,8 +2,8 @@ set SHOW= -awsumepy %* > %HOME%/.aws/awsume-temp.txt -set /p AWSUME_TEXT=<%HOME%/.aws/awsume-temp.txt +awsumepy %* > ./temp.txt +set /p AWSUME_TEXT=<./temp.txt FOR %%A IN (%*) DO ( IF "%%A"=="-s" (set "SHOW=y") @@ -28,15 +28,17 @@ for /f "tokens=1,2,3,4,5,6 delims= " %%a in ("%AWSUME_TEXT%") do ( set AWS_REGION=%%c set AWS_DEFAULT_REGION=%%c) - set AWSUME_PROFILE=%%d + if "%%d" NEQ "None" ( + set AWSUME_PROFILE=%%d) + start /min "autoawsume" autoawsume ) if "%%a" == "Version" ( - awsumepy -v + awsumepy %* ) if "%%a" == "Listing..." ( - awsumepy -l + awsumepy %* ) if "%%a" == "Unset" ( set AWS_SECRET_ACCESS_KEY= @@ -69,7 +71,7 @@ for /f "tokens=1,2,3,4,5,6 delims= " %%a in ("%AWSUME_TEXT%") do ( set AWS_PROFILE= set AWS_DEFAULT_PROFILE= set AWSUME_PROFILE= - taskkill /FI "WindowTitle eq autoawsume" > %HOME%/.aws/awsume-null 2>&1 + taskkill /FI "WindowTitle eq autoawsume" > null 2>&1 ) if "%%a" == "Stop" ( if "auto-refresh-%%b" == "%AWS_PROFILE%" ( @@ -100,7 +102,8 @@ for /f "tokens=1,2,3,4,5,6 delims= " %%a in ("%AWSUME_TEXT%") do ( set AWS_REGION=%%e set AWS_DEFAULT_REGION=%%e) - set AWSUME_PROFILE=%%f + if "%%f" NEQ "None" ( + set AWSUME_PROFILE=%%f) IF defined SHOW ( for /f "tokens=1,2,3,4,5 delims= " %%a in ("%AWSUME_TEXT%") do ( @@ -115,8 +118,10 @@ for /f "tokens=1,2,3,4,5,6 delims= " %%a in ("%AWSUME_TEXT%") do ( echo set AWS_REGION=%%e echo set AWS_DEFAULT_REGION=%%e) - echo set AWSUME_PROFILE=%%f + if "%%f" NEQ "None" ( + echo set AWSUME_PROFILE=%%f) ) ) ) ) + diff --git a/awsume/shellScripts/awsume.fish b/shell_scripts/awsume.fish old mode 100644 new mode 100755 similarity index 92% rename from awsume/shellScripts/awsume.fish rename to shell_scripts/awsume.fish index f0f48a1..f2fa8f2 --- a/awsume/shellScripts/awsume.fish +++ b/shell_scripts/awsume.fish @@ -1,7 +1,7 @@ #!/bin/fish #AWSume - a bash script shell wrapper to awsumepy, a cli that makes using AWS IAM credentials easy -#AWSUME_FLAG - what awsumepy told the shell to +#AWSUME_FLAG - what awsumepy told the shell to #AWSUME_n - the data from awsumepy set -x AWSUME_OUTPUT (awsumepy $argv | tr ' ' '\n') @@ -43,8 +43,9 @@ else if [ "$AWSUME_FLAG" = "Auto" ] export AWS_REGION=$AWSUME_2 export AWS_DEFAULT_REGION=$AWSUME_2 end - - export AWSUME_PROFILE=$AWSUME_3 + if [ ! "$AWSUME_3" = "None" ] + export AWSUME_PROFILE=$AWSUME_3 + end #run the background autoawsume process autoawsume & disown @@ -122,11 +123,14 @@ else if [ "$AWSUME_FLAG" = "Awsume" ] export AWS_DEFAULT_REGION=$AWSUME_4 end - export AWSUME_PROFILE=$AWSUME_5 + if [ ! "$AWSUME_5" = "None" ] + export AWSUME_PROFILE=$AWSUME_5 + end + #if enabled, show the exact commands to use in order to assume the role we just assumed for AWSUME_var in $argv - + #show commands if [ "$AWSUME_var" = "-s" ] echo export AWS_ACCESS_KEY_ID=$AWSUME_1 @@ -142,8 +146,9 @@ else if [ "$AWSUME_FLAG" = "Awsume" ] echo export AWS_DEFAULT_REGION=$AWSUME_4 end - echo export AWSUME_PROFILE=$AWSUME_5 - + if [ ! "$AWSUME_5" = "None" ] + echo export AWSUME_PROFILE=$AWSUME_5 + end end end end diff --git a/awsume/shellScripts/awsume.ps1 b/shell_scripts/awsume.ps1 similarity index 92% rename from awsume/shellScripts/awsume.ps1 rename to shell_scripts/awsume.ps1 index f45a21d..03eb9bb 100644 --- a/awsume/shellScripts/awsume.ps1 +++ b/shell_scripts/awsume.ps1 @@ -7,15 +7,15 @@ $(awsumepy $args) -split '\s+' #if incorrect flag/help if ( $AWSUME_FLAG -eq "usage:" ) { - $(awsumepy -h) + $(awsumepy $args) } #if version flag elseif ( $AWSUME_FLAG -eq "Version" ) { - $(awsumepy -v) + $(awsumepy $args) } #if -l flag passed elseif ( $AWSUME_FLAG -eq "Listing..." ) { - $(awsumepy -l) + $(awsumepy $args) } #set up auto-refreshing role elseif ( $AWSUME_FLAG -eq "Auto" ) { @@ -40,7 +40,9 @@ elseif ( $AWSUME_FLAG -eq "Auto" ) { $env:AWS_DEFAULT_REGION = $AWSUME_2 } - $env:AWSUME_PROFILE = $AWSUME_3 + if ( $AWSUME_3 -ne "None" ) { + $env:AWSUME_PROFILE = $AWSUME_3 + } #run the background autoawsume process Start-Process powershell -ArgumentList "autoawsume" -WindowStyle hidden @@ -119,23 +121,27 @@ elseif ( $AWSUME_FLAG -eq "Awsume") { $env:AWS_DEFAULT_REGION = $AWSUME_4 } - $env:AWSUME_PROFILE = $AWSUME_5 + if ( $AWSUME_5 -ne "None" ) { + $env:AWSUME_PROFILE = $AWSUME_5 + } #if enabled, show the exact commands to use in order to assume the role we just assumed if ($args -like "-s") { Write-Host "`$env:AWS_ACCESS_KEY_ID =" $env:AWS_ACCESS_KEY_ID Write-Host "`$env:AWS_SECRET_ACCESS_KEY =" $env:AWS_SECRET_ACCESS_KEY - + if ( $AWSUME_3 -ne "None" ) { Write-Host "`$env:AWS_SESSION_TOKEN =" $env:AWS_SESSION_TOKEN Write-Host "`$env:AWS_SECURITY_TOKEN =" $env:AWS_SECURITY_TOKEN } - + if ( $AWSUME_4 -ne "None" ) { Write-Host "`$env:AWS_REGION =" $env:AWS_REGION Write-Host "`$env:AWS_DEFAULT_REGION =" $env:AWS_DEFAULT_REGION } - Write-Host "`$env:AWSUME_PROFILE =" $env:AWSUME_PROFILE + if ( $AWSUME_5 -ne "None" ) { + Write-Host "`$env:AWSUME_PROFILE =" $env:AWSUME_PROFILE + } } }