import time
import datetime
import logging
import os
import getpass
import threading
from collections import namedtuple
from dateutil.parser import parse
from dateutil.tz import tzlocal
import botocore.configloader
import botocore.compat
from botocore.compat import total_seconds
from botocore.exceptions import UnknownCredentialError
from botocore.exceptions import PartialCredentialsError
from botocore.exceptions import ConfigNotFound
from botocore.exceptions import InvalidConfigError
from botocore.exceptions import RefreshWithMFAUnsupportedError
from botocore.exceptions import MetadataRetrievalError
from botocore.exceptions import CredentialRetrievalError
from botocore.utils import InstanceMetadataFetcher, parse_key_val_file
from botocore.utils import ContainerMetadataFetcher
logger = logging.getLogger(__name__)
ReadOnlyCredentials = namedtuple('ReadOnlyCredentials',
['access_key', 'secret_key', 'token'])
def create_credential_resolver(session):
profile_name = session.get_config_variable('profile') or 'default'
credential_file = session.get_config_variable('credentials_file')
config_file = session.get_config_variable('config_file')
metadata_timeout = session.get_config_variable('metadata_service_timeout')
num_attempts = session.get_config_variable('metadata_service_num_attempts')
env_provider = EnvProvider()
providers = [
env_provider,
AssumeRoleProvider(
load_config=lambda: session.full_config,
client_creator=session.create_client,
cache={},
profile_name=profile_name,
),
SharedCredentialProvider(
creds_filename=credential_file,
profile_name=profile_name
),
ConfigProvider(config_filename=config_file, profile_name=profile_name),
OriginalEC2Provider(),
BotoProvider(),
ContainerProvider(),
InstanceMetadataProvider(
iam_role_fetcher=InstanceMetadataFetcher(
timeout=metadata_timeout,
num_attempts=num_attempts)
)
]
explicit_profile = session.get_config_variable('profile',
methods=('instance',))
if explicit_profile is not None:
providers.remove(env_provider)
logger.debug('Skipping environment variable credential check'
' because profile name was explicitly set.')
resolver = CredentialResolver(providers=providers)
return resolver
def get_credentials(session):
resolver = create_credential_resolver(session)
return resolver.load_credentials()
def _local_now():
return datetime.datetime.now(tzlocal())
def _parse_if_needed(value):
if isinstance(value, datetime.datetime):
return value
return parse(value)
def _serialize_if_needed(value):
if isinstance(value, datetime.datetime):
return value.strftime('%Y-%m-%dT%H:%M:%S%Z')
return value
def create_assume_role_refresher(client, params):
def refresh():
response = client.assume_role(**params)
credentials = response['Credentials']
return {
'access_key': credentials['AccessKeyId'],
'secret_key': credentials['SecretAccessKey'],
'token': credentials['SessionToken'],
'expiry_time': _serialize_if_needed(credentials['Expiration']),
}
return refresh
def create_mfa_serial_refresher():
def _refresher():
raise RefreshWithMFAUnsupportedError()
return _refresher
class Credentials(object):
def __init__(self, access_key, secret_key, token=None,
method=None):
self.access_key = access_key
self.secret_key = secret_key
self.token = token
if method is None:
method = 'explicit'
self.method = method
self._normalize()
def _normalize(self):
self.access_key = botocore.compat.ensure_unicode(self.access_key)
self.secret_key = botocore.compat.ensure_unicode(self.secret_key)
def get_frozen_credentials(self):
return ReadOnlyCredentials(self.access_key,
self.secret_key,
self.token)
class RefreshableCredentials(Credentials):
_advisory_refresh_timeout = 15 * 60
_mandatory_refresh_timeout = 10 * 60
def __init__(self, access_key, secret_key, token,
expiry_time, refresh_using, method,
time_fetcher=_local_now):
self._refresh_using = refresh_using
self._access_key = access_key
self._secret_key = secret_key
self._token = token
self._expiry_time = expiry_time
self._time_fetcher = time_fetcher
self._refresh_lock = threading.Lock()
self.method = method
self._frozen_credentials = ReadOnlyCredentials(
access_key, secret_key, token)
self._normalize()
def _normalize(self):
self._access_key = botocore.compat.ensure_unicode(self._access_key)
self._secret_key = botocore.compat.ensure_unicode(self._secret_key)
@classmethod
def create_from_metadata(cls, metadata, refresh_using, method):
instance = cls(
access_key=metadata['access_key'],
secret_key=metadata['secret_key'],
token=metadata['token'],
expiry_time=cls._expiry_datetime(metadata['expiry_time']),
method=method,
refresh_using=refresh_using
)
return instance
@property
def access_key(self):
self._refresh()
return self._access_key
@access_key.setter
def access_key(self, value):
self._access_key = value
@property
def secret_key(self):
self._refresh()
return self._secret_key
@secret_key.setter
def secret_key(self, value):
self._secret_key = value
@property
def token(self):
self._refresh()
return self._token
@token.setter
def token(self, value):
self._token = value
def _seconds_remaining(self):
delta = self._expiry_time - self._time_fetcher()
return total_seconds(delta)
def refresh_needed(self, refresh_in=None):
if self._expiry_time is None:
return False
if refresh_in is None:
refresh_in = self._advisory_refresh_timeout
if self._seconds_remaining() >= refresh_in:
return False
logger.debug("Credentials need to be refreshed.")
return True
def _is_expired(self):
return self.refresh_needed(refresh_in=0)
def _refresh(self):
if not self.refresh_needed(self._advisory_refresh_timeout):
return
if self._refresh_lock.acquire(False):
try:
if not self.refresh_needed(self._advisory_refresh_timeout):
return
is_mandatory_refresh = self.refresh_needed(
self._mandatory_refresh_timeout)
self._protected_refresh(is_mandatory=is_mandatory_refresh)
return
finally:
self._refresh_lock.release()
elif self.refresh_needed(self._mandatory_refresh_timeout):
with self._refresh_lock:
if not self.refresh_needed(self._mandatory_refresh_timeout):
return
self._protected_refresh(is_mandatory=True)
def _protected_refresh(self, is_mandatory):
try:
metadata = self._refresh_using()
except Exception as e:
period_name = 'mandatory' if is_mandatory else 'advisory'
logger.warning("Refreshing temporary credentials failed "
"during %s refresh period.",
period_name, exc_info=True)
if is_mandatory:
raise
return
self._set_from_data(metadata)
if self._is_expired():
msg = ("Credentials were refreshed, but the "
"refreshed credentials are still expired.")
logger.warning(msg)
raise RuntimeError(msg)
self._frozen_credentials = ReadOnlyCredentials(
self._access_key, self._secret_key, self._token)
@staticmethod
def _expiry_datetime(time_str):
return parse(time_str)
def _set_from_data(self, data):
self.access_key = data['access_key']
self.secret_key = data['secret_key']
self.token = data['token']
self._expiry_time = parse(data['expiry_time'])
logger.debug("Retrieved credentials will expire at: %s",
self._expiry_time)
self._normalize()
def get_frozen_credentials(self):
self._refresh()
return self._frozen_credentials
class CredentialProvider(object):
METHOD = None
def __init__(self, session=None):
self.session = session
def load(self):
return True
def _extract_creds_from_mapping(self, mapping, *key_names):
found = []
for key_name in key_names:
try:
found.append(mapping[key_name])
except KeyError:
raise PartialCredentialsError(provider=self.METHOD,
cred_var=key_name)
return found
class InstanceMetadataProvider(CredentialProvider):
METHOD = 'iam-role'
def __init__(self, iam_role_fetcher):
self._role_fetcher = iam_role_fetcher
def load(self):
fetcher = self._role_fetcher
metadata = fetcher.retrieve_iam_role_credentials()
if not metadata:
return None
logger.info('Found credentials from IAM Role: %s',
metadata['role_name'])
creds = RefreshableCredentials.create_from_metadata(
metadata,
method=self.METHOD,
refresh_using=fetcher.retrieve_iam_role_credentials,
)
return creds
class EnvProvider(CredentialProvider):
METHOD = 'env'
ACCESS_KEY = 'AWS_ACCESS_KEY_ID'
SECRET_KEY = 'AWS_SECRET_ACCESS_KEY'
TOKENS = ['AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN']
def __init__(self, environ=None, mapping=None):
if environ is None:
environ = os.environ
self.environ = environ
self._mapping = self._build_mapping(mapping)
def _build_mapping(self, mapping):
var_mapping = {}
if mapping is None:
var_mapping['access_key'] = self.ACCESS_KEY
var_mapping['secret_key'] = self.SECRET_KEY
var_mapping['token'] = self.TOKENS
else:
var_mapping['access_key'] = mapping.get(
'access_key', self.ACCESS_KEY)
var_mapping['secret_key'] = mapping.get(
'secret_key', self.SECRET_KEY)
var_mapping['token'] = mapping.get(
'token', self.TOKENS)
if not isinstance(var_mapping['token'], list):
var_mapping['token'] = [var_mapping['token']]
return var_mapping
def load(self):
if self._mapping['access_key'] in self.environ:
logger.info('Found credentials in environment variables.')
access_key, secret_key = self._extract_creds_from_mapping(
self.environ, self._mapping['access_key'],
self._mapping['secret_key'])
token = self._get_session_token()
return Credentials(access_key, secret_key, token,
method=self.METHOD)
else:
return None
def _get_session_token(self):
for token_envvar in self._mapping['token']:
if token_envvar in self.environ:
return self.environ[token_envvar]
class OriginalEC2Provider(CredentialProvider):
METHOD = 'ec2-credentials-file'
CRED_FILE_ENV = 'AWS_CREDENTIAL_FILE'
ACCESS_KEY = 'AWSAccessKeyId'
SECRET_KEY = 'AWSSecretKey'
def __init__(self, environ=None, parser=None):
if environ is None:
environ = os.environ
if parser is None:
parser = parse_key_val_file
self._environ = environ
self._parser = parser
def load(self):
if 'AWS_CREDENTIAL_FILE' in self._environ:
full_path = os.path.expanduser(self._environ['AWS_CREDENTIAL_FILE'])
creds = self._parser(full_path)
if self.ACCESS_KEY in creds:
logger.info('Found credentials in AWS_CREDENTIAL_FILE.')
access_key = creds[self.ACCESS_KEY]
secret_key = creds[self.SECRET_KEY]
return Credentials(access_key, secret_key, method=self.METHOD)
else:
return None
class SharedCredentialProvider(CredentialProvider):
METHOD = 'shared-credentials-file'
ACCESS_KEY = 'aws_access_key_id'
SECRET_KEY = 'aws_secret_access_key'
TOKENS = ['aws_security_token', 'aws_session_token']
def __init__(self, creds_filename, profile_name=None, ini_parser=None):
self._creds_filename = creds_filename
if profile_name is None:
profile_name = 'default'
self._profile_name = profile_name
if ini_parser is None:
ini_parser = botocore.configloader.raw_config_parse
self._ini_parser = ini_parser
def load(self):
try:
available_creds = self._ini_parser(self._creds_filename)
except ConfigNotFound:
return None
if self._profile_name in available_creds:
config = available_creds[self._profile_name]
if self.ACCESS_KEY in config:
logger.info("Found credentials in shared credentials file: %s",
self._creds_filename)
access_key, secret_key = self._extract_creds_from_mapping(
config, self.ACCESS_KEY, self.SECRET_KEY)
token = self._get_session_token(config)
return Credentials(access_key, secret_key, token,
method=self.METHOD)
def _get_session_token(self, config):
for token_envvar in self.TOKENS:
if token_envvar in config:
return config[token_envvar]
class ConfigProvider(CredentialProvider):
METHOD = 'config-file'
ACCESS_KEY = 'aws_access_key_id'
SECRET_KEY = 'aws_secret_access_key'
TOKENS = ['aws_security_token', 'aws_session_token']
def __init__(self, config_filename, profile_name, config_parser=None):
self._config_filename = config_filename
self._profile_name = profile_name
if config_parser is None:
config_parser = botocore.configloader.load_config
self._config_parser = config_parser
def load(self):
try:
full_config = self._config_parser(self._config_filename)
except ConfigNotFound:
return None
if self._profile_name in full_config['profiles']:
profile_config = full_config['profiles'][self._profile_name]
if self.ACCESS_KEY in profile_config:
logger.info("Credentials found in config file: %s",
self._config_filename)
access_key, secret_key = self._extract_creds_from_mapping(
profile_config, self.ACCESS_KEY, self.SECRET_KEY)
token = self._get_session_token(profile_config)
return Credentials(access_key, secret_key, token,
method=self.METHOD)
else:
return None
def _get_session_token(self, profile_config):
for token_name in self.TOKENS:
if token_name in profile_config:
return profile_config[token_name]
class BotoProvider(CredentialProvider):
METHOD = 'boto-config'
BOTO_CONFIG_ENV = 'BOTO_CONFIG'
DEFAULT_CONFIG_FILENAMES = ['/etc/boto.cfg', '~/.boto']
ACCESS_KEY = 'aws_access_key_id'
SECRET_KEY = 'aws_secret_access_key'
def __init__(self, environ=None, ini_parser=None):
if environ is None:
environ = os.environ
if ini_parser is None:
ini_parser = botocore.configloader.raw_config_parse
self._environ = environ
self._ini_parser = ini_parser
def load(self):
if self.BOTO_CONFIG_ENV in self._environ:
potential_locations = [self._environ[self.BOTO_CONFIG_ENV]]
else:
potential_locations = self.DEFAULT_CONFIG_FILENAMES
for filename in potential_locations:
try:
config = self._ini_parser(filename)
except ConfigNotFound:
continue
if 'Credentials' in config:
credentials = config['Credentials']
if self.ACCESS_KEY in credentials:
logger.info("Found credentials in boto config file: %s",
filename)
access_key, secret_key = self._extract_creds_from_mapping(
credentials, self.ACCESS_KEY, self.SECRET_KEY)
return Credentials(access_key, secret_key,
method=self.METHOD)
class AssumeRoleProvider(CredentialProvider):
METHOD = 'assume-role'
ROLE_CONFIG_VAR = 'role_arn'
EXPIRY_WINDOW_SECONDS = 60 * 15
def __init__(self, load_config, client_creator, cache, profile_name,
prompter=getpass.getpass):
self.cache = cache
self._load_config = load_config
self._client_creator = client_creator
self._profile_name = profile_name
self._prompter = prompter
self._loaded_config = {}
def load(self):
self._loaded_config = self._load_config()
if self._has_assume_role_config_vars():
return self._load_creds_via_assume_role()
def _has_assume_role_config_vars(self):
profiles = self._loaded_config.get('profiles', {})
return self.ROLE_CONFIG_VAR in profiles.get(self._profile_name, {})
def _load_creds_via_assume_role(self):
creds = self._load_creds_from_cache()
if creds is not None:
logger.debug("Credentials for role retrieved from cache.")
return creds
else:
creds, response = self._retrieve_temp_credentials()
cache_key = self._create_cache_key()
self._write_cached_credentials(response, cache_key)
return creds
def _load_creds_from_cache(self):
cache_key = self._create_cache_key()
try:
from_cache = self.cache[cache_key]
if self._is_expired(from_cache):
logger.debug(
"Credentials were found in cache, but they are expired.")
return None
else:
return self._create_creds_from_response(from_cache)
except KeyError:
return None
def _is_expired(self, credentials):
end_time = parse(credentials['Credentials']['Expiration'])
now = datetime.datetime.now(tzlocal())
seconds = total_seconds(end_time - now)
return seconds < self.EXPIRY_WINDOW_SECONDS
def _create_cache_key(self):
role_config = self._get_role_config_values()
role_arn = role_config['role_arn'].replace(':', '_')
role_session_name = role_config.get('role_session_name')
if role_session_name:
cache_key = '%s--%s--%s' % (self._profile_name, role_arn,
role_session_name)
else:
cache_key = '%s--%s' % (self._profile_name, role_arn)
return cache_key.replace('/', '-')
def _write_cached_credentials(self, creds, cache_key):
self.cache[cache_key] = creds
def _get_role_config_values(self):
profiles = self._loaded_config.get('profiles', {})
try:
source_profile = profiles[self._profile_name]['source_profile']
role_arn = profiles[self._profile_name]['role_arn']
mfa_serial = profiles[self._profile_name].get('mfa_serial')
except KeyError as e:
raise PartialCredentialsError(provider=self.METHOD,
cred_var=str(e))
external_id = profiles[self._profile_name].get('external_id')
role_session_name = \
profiles[self._profile_name].get('role_session_name')
if source_profile not in profiles:
raise InvalidConfigError(
error_msg=(
'The source_profile "%s" referenced in '
'the profile "%s" does not exist.' % (
source_profile, self._profile_name)))
source_cred_values = profiles[source_profile]
return {
'role_arn': role_arn,
'external_id': external_id,
'source_profile': source_profile,
'mfa_serial': mfa_serial,
'source_cred_values': source_cred_values,
'role_session_name': role_session_name
}
def _create_creds_from_response(self, response):
config = self._get_role_config_values()
if config.get('mfa_serial') is not None:
refresh_func = create_mfa_serial_refresher()
else:
refresh_func = create_assume_role_refresher(
self._create_client_from_config(config),
self._assume_role_base_kwargs(config))
return RefreshableCredentials(
access_key=response['Credentials']['AccessKeyId'],
secret_key=response['Credentials']['SecretAccessKey'],
token=response['Credentials']['SessionToken'],
method=self.METHOD,
expiry_time=_parse_if_needed(
response['Credentials']['Expiration']),
refresh_using=refresh_func)
def _create_client_from_config(self, config):
source_cred_values = config['source_cred_values']
client = self._client_creator(
'sts', aws_access_key_id=source_cred_values['aws_access_key_id'],
aws_secret_access_key=source_cred_values['aws_secret_access_key'],
aws_session_token=source_cred_values.get('aws_session_token'),
)
return client
def _retrieve_temp_credentials(self):
logger.debug("Retrieving credentials via AssumeRole.")
config = self._get_role_config_values()
client = self._create_client_from_config(config)
assume_role_kwargs = self._assume_role_base_kwargs(config)
response = client.assume_role(**assume_role_kwargs)
creds = self._create_creds_from_response(response)
return creds, response
def _assume_role_base_kwargs(self, config):
assume_role_kwargs = {'RoleArn': config['role_arn']}
if config['external_id'] is not None:
assume_role_kwargs['ExternalId'] = config['external_id']
if config['mfa_serial'] is not None:
token_code = self._prompter("Enter MFA code: ")
assume_role_kwargs['SerialNumber'] = config['mfa_serial']
assume_role_kwargs['TokenCode'] = token_code
if config['role_session_name'] is not None:
assume_role_kwargs['RoleSessionName'] = config['role_session_name']
else:
role_session_name = 'AWS-CLI-session-%s' % (int(time.time()))
assume_role_kwargs['RoleSessionName'] = role_session_name
return assume_role_kwargs
class ContainerProvider(CredentialProvider):
METHOD = 'container-role'
ENV_VAR = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'
def __init__(self, environ=None, fetcher=None):
if environ is None:
environ = os.environ
if fetcher is None:
fetcher = ContainerMetadataFetcher()
self._environ = environ
self._fetcher = fetcher
def load(self):
if self.ENV_VAR not in self._environ:
return None
return self._retrieve_or_fail(self._environ[self.ENV_VAR])
def _retrieve_or_fail(self, relative_uri):
fetcher = self._create_fetcher(relative_uri)
creds = fetcher()
return RefreshableCredentials(
access_key=creds['access_key'],
secret_key=creds['secret_key'],
token=creds['token'],
method=self.METHOD,
expiry_time=_parse_if_needed(creds['expiry_time']),
refresh_using=fetcher,
)
def _create_fetcher(self, relative_uri):
def fetch_creds():
try:
response = self._fetcher.retrieve_uri(relative_uri)
except MetadataRetrievalError as e:
logger.debug("Error retrieving ECS metadata: %s", e,
exc_info=True)
raise CredentialRetrievalError(provider=self.METHOD,
error_msg=str(e))
return {
'access_key': response['AccessKeyId'],
'secret_key': response['SecretAccessKey'],
'token': response['Token'],
'expiry_time': response['Expiration'],
}
return fetch_creds
class CredentialResolver(object):
def __init__(self, providers):
self.providers = providers
def insert_before(self, name, credential_provider):
try:
offset = [p.METHOD for p in self.providers].index(name)
except ValueError:
raise UnknownCredentialError(name=name)
self.providers.insert(offset, credential_provider)
def insert_after(self, name, credential_provider):
offset = self._get_provider_offset(name)
self.providers.insert(offset + 1, credential_provider)
def remove(self, name):
available_methods = [p.METHOD for p in self.providers]
if name not in available_methods:
return
offset = available_methods.index(name)
self.providers.pop(offset)
def get_provider(self, name):
return self.providers[self._get_provider_offset(name)]
def _get_provider_offset(self, name):
try:
return [p.METHOD for p in self.providers].index(name)
except ValueError:
raise UnknownCredentialError(name=name)
def load_credentials(self):
for provider in self.providers:
logger.debug("Looking for credentials via: %s", provider.METHOD)
creds = provider.load()
if creds is not None:
return creds
return None