def initial_setup(logger, config, config_path, no_keychain=False): console_input = prompter() profile_name = console_input('Profile name to [%s]: ' % ("default")) if profile_name is None or profile_name == "": profile_name = "default" profile_name = "{}-long-term".format(profile_name) aws_access_key_id = getpass.getpass('aws_access_key_id: ') if aws_access_key_id is None or aws_access_key_id == "": log_error_and_exit(logger, "You must supply aws_access_key_id") aws_secret_access_key = getpass.getpass('aws_secret_access_key: ') if aws_secret_access_key is None or aws_secret_access_key == "": log_error_and_exit(logger, "You must supply aws_secret_access_key") if no_keychain: config.add_section(profile_name) config.set(profile_name, 'aws_access_key_id', aws_access_key_id) config.set(profile_name, 'aws_secret_access_key', aws_secret_access_key) with open(config_path, 'w') as configfile: config.write(configfile) else: keyring.set_password("aws:access_key_id", profile_name, aws_access_key_id) keyring.set_password("aws:secret_access_key", profile_name, aws_secret_access_key)
def get_config(aws_creds_path): config = configparser.RawConfigParser() try: config.read(aws_creds_path) except configparser.ParsingError: e = sys.exc_info()[1] log_error_and_exit(logger, "There was a problem reading or parsing " "your credentials file: %s" % (e.args[0],)) return config
def get_credentials(short_term_name, lt_key_id, lt_access_key, args, config): if args.token: logger.debug("Received token as argument") mfa_token = '%s' % (args.token) else: console_input = prompter() mfa_token = console_input('Enter AWS MFA code for device [%s] ' '(renewing for %s seconds):' % (args.device, args.duration)) client = boto3.client('sts', aws_access_key_id=lt_key_id, aws_secret_access_key=lt_access_key) if args.assume_role: logger.info("Assuming Role - Profile: %s, Role: %s, Duration: %s", short_term_name, args.assume_role, args.duration) if args.role_session_name is None: log_error_and_exit( logger, "You must specify a role session name " "via --role-session-name") try: response = client.assume_role( RoleArn=args.assume_role, RoleSessionName=args.role_session_name, DurationSeconds=args.duration, SerialNumber=args.device, TokenCode=mfa_token) except ClientError as e: log_error_and_exit( logger, "An error occured while calling " "assume role: {}".format(e)) except ParamValidationError: log_error_and_exit(logger, "Token must be six digits") config.set( short_term_name, 'assumed_role', 'True', ) config.set( short_term_name, 'assumed_role_arn', args.assume_role, ) else: logger.info("Fetching Credentials - Profile: %s, Duration: %s", short_term_name, args.duration) try: response = client.get_session_token(DurationSeconds=args.duration, SerialNumber=args.device, TokenCode=mfa_token) except ClientError as e: log_error_and_exit( logger, "An error occured while calling assume role: {}".format(e)) except ParamValidationError: log_error_and_exit(logger, "Token must be six digits") config.set( short_term_name, 'assumed_role', 'False', ) config.remove_option(short_term_name, 'assumed_role_arn') # aws_session_token and aws_security_token are both added # to support boto and boto3 options = [ ('aws_access_key_id', 'AccessKeyId'), ('aws_secret_access_key', 'SecretAccessKey'), ('aws_session_token', 'SessionToken'), ('aws_security_token', 'SessionToken'), ] for option, value in options: config.set(short_term_name, option, response['Credentials'][value]) # Save expiration individiually, so it can be manipulated config.set( short_term_name, 'expiration', response['Credentials']['Expiration'].strftime('%Y-%m-%d %H:%M:%S')) with open(AWS_CREDS_PATH, 'w') as configfile: config.write(configfile) logger.info("Success! Your credentials will expire in %s seconds at: %s" % (args.duration, response['Credentials']['Expiration'])) sys.exit(0)
def main(): parser = argparse.ArgumentParser() parser.add_argument('--device', required=False, metavar='arn:aws:iam::123456788990:mfa/dudeman', help="The MFA Device ARN. This value can also be " "provided via the environment variable 'MFA_DEVICE' or" " the ~/.aws/credentials variable 'aws_mfa_device'.") parser.add_argument('--duration', type=int, help="The duration, in seconds, that the temporary " "credentials should remain valid. Minimum value: " "900 (15 minutes). Maximum: 129600 (36 hours). " "Defaults to 43200 (12 hours), or 3600 (one " "hour) when using '--assume-role'. This value " "can also be provided via the environment " "variable 'MFA_STS_DURATION'. ") parser.add_argument('--profile', help="If using profiles, specify the name here. The " "default profile name is 'default'. The value can " "also be provided via the environment variable " "'AWS_PROFILE'.", required=False) parser.add_argument('--long-term-suffix', '--long-suffix', help="The suffix appended to the profile name to" "identify the long term credential section", required=False) parser.add_argument('--short-term-suffix', '--short-suffix', help="The suffix appended to the profile name to" "identify the short term credential section", required=False) parser.add_argument('--assume-role', '--assume', metavar='arn:aws:iam::123456788990:role/RoleName', help="The ARN of the AWS IAM Role you would like to " "assume, if specified. This value can also be provided" " via the environment variable 'MFA_ASSUME_ROLE'", required=False) parser.add_argument('--role-session-name', help="Friendly session name required when using " "--assume-role", default=getpass.getuser(), required=False) parser.add_argument('--force', help="Refresh credentials even if currently valid.", action="store_true", required=False) parser.add_argument( '--log-level', help="Set log level", choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'], required=False, default='DEBUG') parser.add_argument('--setup', help="Setup a new long term credentials section", action="store_true", required=False) parser.add_argument('--token', '--mfa-token', type=str, help="Provide MFA token as an argument", required=False) args = parser.parse_args() level = getattr(logging, args.log_level) setup_logger(level) if not os.path.isfile(AWS_CREDS_PATH): console_input = prompter() create = console_input("Could not locate credentials file at {}, " "would you like to create one? " "[y/n]".format(AWS_CREDS_PATH)) if create.lower() == "y": with open(AWS_CREDS_PATH, 'a'): pass else: log_error_and_exit( logger, 'Could not locate credentials file at ' '%s' % (AWS_CREDS_PATH, )) config = get_config(AWS_CREDS_PATH) if args.setup: initial_setup(logger, config, AWS_CREDS_PATH) return validate(args, config)
def validate(args, config): if not args.profile: if os.environ.get('AWS_PROFILE'): args.profile = os.environ.get('AWS_PROFILE') else: args.profile = 'default' if not args.long_term_suffix: long_term_name = '%s-long-term' % (args.profile, ) elif args.long_term_suffix.lower() == 'none': long_term_name = args.profile else: long_term_name = '%s-%s' % (args.profile, args.long_term_suffix) if not args.short_term_suffix or args.short_term_suffix.lower() == 'none': short_term_name = args.profile else: short_term_name = '%s-%s' % (args.profile, args.short_term_suffix) if long_term_name == short_term_name: log_error_and_exit( logger, "The value for '--long-term-suffix' cannot " "be equal to the value for '--short-term-suffix'") if args.assume_role: role_msg = "with assumed role: %s" % (args.assume_role, ) elif config.has_option(args.profile, 'assumed_role_arn'): role_msg = "with assumed role: %s" % (config.get( args.profile, 'assumed_role_arn')) else: role_msg = "" logger.info('Validating credentials for profile: %s %s' % (short_term_name, role_msg)) reup_message = "Obtaining credentials for a new role or profile." try: key_id = config.get(long_term_name, 'aws_access_key_id') access_key = config.get(long_term_name, 'aws_secret_access_key') except NoSectionError: log_error_and_exit( logger, "Long term credentials session '[%s]' is missing. " "You must add this section to your credentials file " "along with your long term 'aws_access_key_id' and " "'aws_secret_access_key'" % (long_term_name, )) except NoOptionError as e: log_error_and_exit(logger, e) # get device from param, env var or config if not args.device: if os.environ.get('MFA_DEVICE'): args.device = os.environ.get('MFA_DEVICE') elif config.has_option(long_term_name, 'aws_mfa_device'): args.device = config.get(long_term_name, 'aws_mfa_device') else: log_error_and_exit( logger, 'You must provide --device or MFA_DEVICE or set ' '"aws_mfa_device" in ".aws/credentials"') # get assume_role from param or env var if not args.assume_role: if os.environ.get('MFA_ASSUME_ROLE'): args.assume_role = os.environ.get('MFA_ASSUME_ROLE') elif config.has_option(long_term_name, 'assume_role'): args.assume_role = config.get(long_term_name, 'assume_role') # get duration from param, env var or set default if not args.duration: if os.environ.get('MFA_STS_DURATION'): args.duration = int(os.environ.get('MFA_STS_DURATION')) else: args.duration = 3600 if args.assume_role else 43200 # If this is False, only refresh credentials if expired. Otherwise # always refresh. force_refresh = False # Validate presence of short-term section if not config.has_section(short_term_name): logger.info("Short term credentials section %s is missing, " "obtaining new credentials." % (short_term_name, )) if short_term_name == 'default': try: config.add_section(short_term_name) # a hack for creating a section named "default" except ValueError: configparser.DEFAULTSECT = short_term_name config.set(short_term_name, 'CREATE', 'TEST') config.remove_option(short_term_name, 'CREATE') else: config.add_section(short_term_name) force_refresh = True # Validate option integrity of short-term section else: required_options = [ 'assumed_role', 'aws_access_key_id', 'aws_secret_access_key', 'aws_session_token', 'aws_security_token', 'expiration' ] try: short_term = {} for option in required_options: short_term[option] = config.get(short_term_name, option) except NoOptionError: logger.warn("Your existing credentials are missing or invalid, " "obtaining new credentials.") force_refresh = True try: current_role = config.get(short_term_name, 'assumed_role_arn') except NoOptionError: current_role = None if args.force: logger.info("Forcing refresh of credentials.") force_refresh = True # There are not credentials for an assumed role, # but the user is trying to assume one elif current_role is None and args.assume_role: logger.info(reup_message) force_refresh = True # There are current credentials for a role and # the role arn being provided is the same. elif (current_role is not None and args.assume_role and current_role == args.assume_role): pass # There are credentials for a current role and the role # that is attempting to be assumed is different elif (current_role is not None and args.assume_role and current_role != args.assume_role): logger.info(reup_message) force_refresh = True # There are credentials for a current role and no role arn is # being supplied elif current_role is not None and args.assume_role is None: logger.info(reup_message) force_refresh = True should_refresh = True # Unless we're forcing a refresh, check expiration. if not force_refresh: exp = datetime.datetime.strptime( config.get(short_term_name, 'expiration'), '%Y-%m-%d %H:%M:%S') diff = exp - datetime.datetime.utcnow() if diff.total_seconds() <= 0: logger.info("Your credentials have expired, renewing.") else: should_refresh = False logger.info("Your credentials are still valid for %s seconds" " they will expire at %s" % (diff.total_seconds(), exp)) if should_refresh: get_credentials(short_term_name, key_id, access_key, args, config)
def get_credentials(short_term_name, long_term_name, lt_key_id, lt_access_key, args, config): if args.token: logger.debug("Received token as argument") mfa_token = '%s' % (args.token) else: console_input = prompter() mfa_token = console_input('Enter AWS MFA code for device [%s] ' '(renewing for %s seconds):' % (args.device, args.duration)) client = boto3.client('sts', aws_access_key_id=lt_key_id, aws_secret_access_key=lt_access_key) if args.assume_role: logger.info("Assuming Role - Profile: %s, Role: %s, Duration: %s", short_term_name, args.assume_role, args.duration) if args.role_session_name is None: log_error_and_exit( logger, "You must specify a role session name " "via --role-session-name") try: response = client.assume_role( RoleArn=args.assume_role, RoleSessionName=args.role_session_name, DurationSeconds=args.duration, SerialNumber=args.device, TokenCode=mfa_token) except ClientError as e: log_error_and_exit( logger, "An error occured while calling " "assume role: {}".format(e)) except ParamValidationError as e: log_error_and_exit(logger, getattr(e, 'message', repr(e))) config.set( short_term_name, 'assumed_role', 'True', ) config.set( short_term_name, 'assumed_role_arn', args.assume_role, ) else: logger.info("Fetching Credentials - Profile: %s, Duration: %s", short_term_name, args.duration) try: response = client.get_session_token(DurationSeconds=args.duration, SerialNumber=args.device, TokenCode=mfa_token) except ClientError as e: log_error_and_exit( logger, "An error occured while calling assume role: {}".format(e)) except ParamValidationError: log_error_and_exit(logger, getattr(e, 'message', repr(e))) config.set( short_term_name, 'assumed_role', 'False', ) config.remove_option(short_term_name, 'assumed_role_arn') # aws_session_token and aws_security_token are both added # to support boto and boto3 options = [ ('aws_access_key_id', 'AccessKeyId'), ('aws_secret_access_key', 'SecretAccessKey'), ('aws_session_token', 'SessionToken'), ('aws_security_token', 'SessionToken'), ] for option, value in options: config.set(short_term_name, option, response['Credentials'][value]) # Save expiration individiually, so it can be manipulated config.set( short_term_name, 'expiration', response['Credentials']['Expiration'].strftime('%Y-%m-%d %H:%M:%S')) # Copy over other aws config options # List taken from - https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html # Excluding those to do with authentication, which we have already handled copy_options = [ 'region', 's3', 'api_versions', 'ca_bundle', 'cli_follow_urlparam', 'cli_timestamp_format', 'output', 'parameter_validation', 'tcp_keepalive' ] for copy_option in copy_options: if config.has_option(long_term_name, copy_option): config.set(short_term_name, copy_option, config.get(long_term_name, copy_option)) with open(AWS_CREDS_PATH, 'w') as configfile: config.write(configfile) logger.info("Success! Your credentials will expire in %s seconds at: %s" % (args.duration, response['Credentials']['Expiration'])) warn_on_existing_aws_env() sys.exit(0)