codekingpro/portable-devtools
114k
1# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/2# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.3#4# Licensed under the Apache License, Version 2.0 (the "License"). You5# may not use this file except in compliance with the License. A copy of6# the License is located at7#8# http://aws.amazon.com/apache2.0/9#10# or in the "license" file accompanying this file. This file is11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF12# ANY KIND, either express or implied. See the License for the specific13# language governing permissions and limitations under the License.14import datetime15import getpass16import json17import logging18import os19import subprocess20import threading21import time22from collections import namedtuple23from copy import deepcopy24from hashlib import sha125 26from dateutil.parser import parse27from dateutil.tz import tzlocal, tzutc28 29import botocore.compat30import botocore.configloader31from botocore import UNSIGNED32from botocore.compat import compat_shell_split, total_seconds33from botocore.config import Config34from botocore.exceptions import (35 ConfigNotFound,36 CredentialRetrievalError,37 InfiniteLoopConfigError,38 InvalidConfigError,39 MetadataRetrievalError,40 PartialCredentialsError,41 RefreshWithMFAUnsupportedError,42 UnauthorizedSSOTokenError,43 UnknownCredentialError,44)45from botocore.tokens import SSOTokenProvider46from botocore.utils import (47 ContainerMetadataFetcher,48 FileWebIdentityTokenLoader,49 InstanceMetadataFetcher,50 JSONFileCache,51 SSOTokenLoader,52 parse_key_val_file,53 resolve_imds_endpoint_mode,54)55 56logger = logging.getLogger(__name__)57ReadOnlyCredentials = namedtuple(58 'ReadOnlyCredentials', ['access_key', 'secret_key', 'token']59)60 61_DEFAULT_MANDATORY_REFRESH_TIMEOUT = 10 * 60 # 10 min62_DEFAULT_ADVISORY_REFRESH_TIMEOUT = 15 * 60 # 15 min63 64 65def create_credential_resolver(session, cache=None, region_name=None):66 """Create a default credential resolver.67 68 This creates a pre-configured credential resolver69 that includes the default lookup chain for70 credentials.71 72 """73 profile_name = session.get_config_variable('profile') or 'default'74 metadata_timeout = session.get_config_variable('metadata_service_timeout')75 num_attempts = session.get_config_variable('metadata_service_num_attempts')76 disable_env_vars = session.instance_variables().get('profile') is not None77 78 imds_config = {79 'ec2_metadata_service_endpoint': session.get_config_variable(80 'ec2_metadata_service_endpoint'81 ),82 'ec2_metadata_service_endpoint_mode': resolve_imds_endpoint_mode(83 session84 ),85 'ec2_credential_refresh_window': _DEFAULT_ADVISORY_REFRESH_TIMEOUT,86 'ec2_metadata_v1_disabled': session.get_config_variable(87 'ec2_metadata_v1_disabled'88 ),89 }90 91 if cache is None:92 cache = {}93 94 env_provider = EnvProvider()95 container_provider = ContainerProvider()96 instance_metadata_provider = InstanceMetadataProvider(97 iam_role_fetcher=InstanceMetadataFetcher(98 timeout=metadata_timeout,99 num_attempts=num_attempts,100 user_agent=session.user_agent(),101 config=imds_config,102 )103 )104 105 profile_provider_builder = ProfileProviderBuilder(106 session, cache=cache, region_name=region_name107 )108 assume_role_provider = AssumeRoleProvider(109 load_config=lambda: session.full_config,110 client_creator=_get_client_creator(session, region_name),111 cache=cache,112 profile_name=profile_name,113 credential_sourcer=CanonicalNameCredentialSourcer(114 [env_provider, container_provider, instance_metadata_provider]115 ),116 profile_provider_builder=profile_provider_builder,117 )118 119 pre_profile = [120 env_provider,121 assume_role_provider,122 ]123 profile_providers = profile_provider_builder.providers(124 profile_name=profile_name,125 disable_env_vars=disable_env_vars,126 )127 post_profile = [128 OriginalEC2Provider(),129 BotoProvider(),130 container_provider,131 instance_metadata_provider,132 ]133 providers = pre_profile + profile_providers + post_profile134 135 if disable_env_vars:136 # An explicitly provided profile will negate an EnvProvider.137 # We will defer to providers that understand the "profile"138 # concept to retrieve credentials.139 # The one edge case if is all three values are provided via140 # env vars:141 # export AWS_ACCESS_KEY_ID=foo142 # export AWS_SECRET_ACCESS_KEY=bar143 # export AWS_PROFILE=baz144 # Then, just like our client() calls, the explicit credentials145 # will take precedence.146 #147 # This precedence is enforced by leaving the EnvProvider in the chain.148 # This means that the only way a "profile" would win is if the149 # EnvProvider does not return credentials, which is what we want150 # in this scenario.151 providers.remove(env_provider)152 logger.debug(153 'Skipping environment variable credential check'154 ' because profile name was explicitly set.'155 )156 157 resolver = CredentialResolver(providers=providers)158 return resolver159 160 161class ProfileProviderBuilder:162 """This class handles the creation of profile based providers.163 164 NOTE: This class is only intended for internal use.165 166 This class handles the creation and ordering of the various credential167 providers that primarly source their configuration from the shared config.168 This is needed to enable sharing between the default credential chain and169 the source profile chain created by the assume role provider.170 """171 172 def __init__(173 self, session, cache=None, region_name=None, sso_token_cache=None174 ):175 self._session = session176 self._cache = cache177 self._region_name = region_name178 self._sso_token_cache = sso_token_cache179 180 def providers(self, profile_name, disable_env_vars=False):181 return [182 self._create_web_identity_provider(183 profile_name,184 disable_env_vars,185 ),186 self._create_sso_provider(profile_name),187 self._create_shared_credential_provider(profile_name),188 self._create_process_provider(profile_name),189 self._create_config_provider(profile_name),190 ]191 192 def _create_process_provider(self, profile_name):193 return ProcessProvider(194 profile_name=profile_name,195 load_config=lambda: self._session.full_config,196 )197 198 def _create_shared_credential_provider(self, profile_name):199 credential_file = self._session.get_config_variable('credentials_file')200 return SharedCredentialProvider(201 profile_name=profile_name,202 creds_filename=credential_file,203 )204 205 def _create_config_provider(self, profile_name):206 config_file = self._session.get_config_variable('config_file')207 return ConfigProvider(208 profile_name=profile_name,209 config_filename=config_file,210 )211 212 def _create_web_identity_provider(self, profile_name, disable_env_vars):213 return AssumeRoleWithWebIdentityProvider(214 load_config=lambda: self._session.full_config,215 client_creator=_get_client_creator(216 self._session, self._region_name217 ),218 cache=self._cache,219 profile_name=profile_name,220 disable_env_vars=disable_env_vars,221 )222 223 def _create_sso_provider(self, profile_name):224 return SSOProvider(225 load_config=lambda: self._session.full_config,226 client_creator=self._session.create_client,227 profile_name=profile_name,228 cache=self._cache,229 token_cache=self._sso_token_cache,230 token_provider=SSOTokenProvider(231 self._session,232 cache=self._sso_token_cache,233 profile_name=profile_name,234 ),235 )236 237 238def get_credentials(session):239 resolver = create_credential_resolver(session)240 return resolver.load_credentials()241 242 243def _local_now():244 return datetime.datetime.now(tzlocal())245 246 247def _parse_if_needed(value):248 if isinstance(value, datetime.datetime):249 return value250 return parse(value)251 252 253def _serialize_if_needed(value, iso=False):254 if isinstance(value, datetime.datetime):255 if iso:256 return value.isoformat()257 return value.strftime('%Y-%m-%dT%H:%M:%S%Z')258 return value259 260 261def _get_client_creator(session, region_name):262 def client_creator(service_name, **kwargs):263 create_client_kwargs = {'region_name': region_name}264 create_client_kwargs.update(**kwargs)265 return session.create_client(service_name, **create_client_kwargs)266 267 return client_creator268 269 270def create_assume_role_refresher(client, params):271 def refresh():272 response = client.assume_role(**params)273 credentials = response['Credentials']274 # We need to normalize the credential names to275 # the values expected by the refresh creds.276 return {277 'access_key': credentials['AccessKeyId'],278 'secret_key': credentials['SecretAccessKey'],279 'token': credentials['SessionToken'],280 'expiry_time': _serialize_if_needed(credentials['Expiration']),281 }282 283 return refresh284 285 286def create_mfa_serial_refresher(actual_refresh):287 class _Refresher:288 def __init__(self, refresh):289 self._refresh = refresh290 self._has_been_called = False291 292 def __call__(self):293 if self._has_been_called:294 # We can explore an option in the future to support295 # reprompting for MFA, but for now we just error out296 # when the temp creds expire.297 raise RefreshWithMFAUnsupportedError()298 self._has_been_called = True299 return self._refresh()300 301 return _Refresher(actual_refresh)302 303 304class Credentials:305 """306 Holds the credentials needed to authenticate requests.307 308 :param str access_key: The access key part of the credentials.309 :param str secret_key: The secret key part of the credentials.310 :param str token: The security token, valid only for session credentials.311 :param str method: A string which identifies where the credentials312 were found.313 """314 315 def __init__(self, access_key, secret_key, token=None, method=None):316 self.access_key = access_key317 self.secret_key = secret_key318 self.token = token319 320 if method is None:321 method = 'explicit'322 self.method = method323 324 self._normalize()325 326 def _normalize(self):327 # Keys would sometimes (accidentally) contain non-ascii characters.328 # It would cause a confusing UnicodeDecodeError in Python 2.329 # We explicitly convert them into unicode to avoid such error.330 #331 # Eventually the service will decide whether to accept the credential.332 # This also complies with the behavior in Python 3.333 self.access_key = botocore.compat.ensure_unicode(self.access_key)334 self.secret_key = botocore.compat.ensure_unicode(self.secret_key)335 336 def get_frozen_credentials(self):337 return ReadOnlyCredentials(338 self.access_key, self.secret_key, self.token339 )340 341 342class RefreshableCredentials(Credentials):343 """344 Holds the credentials needed to authenticate requests. In addition, it345 knows how to refresh itself.346 347 :param str access_key: The access key part of the credentials.348 :param str secret_key: The secret key part of the credentials.349 :param str token: The security token, valid only for session credentials.350 :param datetime expiry_time: The expiration time of the credentials.351 :param function refresh_using: Callback function to refresh the credentials.352 :param str method: A string which identifies where the credentials353 were found.354 :param function time_fetcher: Callback function to retrieve current time.355 """356 357 # The time at which we'll attempt to refresh, but not358 # block if someone else is refreshing.359 _advisory_refresh_timeout = _DEFAULT_ADVISORY_REFRESH_TIMEOUT360 # The time at which all threads will block waiting for361 # refreshed credentials.362 _mandatory_refresh_timeout = _DEFAULT_MANDATORY_REFRESH_TIMEOUT363 364 def __init__(365 self,366 access_key,367 secret_key,368 token,369 expiry_time,370 refresh_using,371 method,372 time_fetcher=_local_now,373 advisory_timeout=None,374 mandatory_timeout=None,375 ):376 self._refresh_using = refresh_using377 self._access_key = access_key378 self._secret_key = secret_key379 self._token = token380 self._expiry_time = expiry_time381 self._time_fetcher = time_fetcher382 self._refresh_lock = threading.Lock()383 self.method = method384 self._frozen_credentials = ReadOnlyCredentials(385 access_key, secret_key, token386 )387 self._normalize()388 if advisory_timeout is not None:389 self._advisory_refresh_timeout = advisory_timeout390 if mandatory_timeout is not None:391 self._mandatory_refresh_timeout = mandatory_timeout392 393 def _normalize(self):394 self._access_key = botocore.compat.ensure_unicode(self._access_key)395 self._secret_key = botocore.compat.ensure_unicode(self._secret_key)396 397 @classmethod398 def create_from_metadata(399 cls,400 metadata,401 refresh_using,402 method,403 advisory_timeout=None,404 mandatory_timeout=None,405 ):406 kwargs = {}407 if advisory_timeout is not None:408 kwargs['advisory_timeout'] = advisory_timeout409 if mandatory_timeout is not None:410 kwargs['mandatory_timeout'] = mandatory_timeout411 412 instance = cls(413 access_key=metadata['access_key'],414 secret_key=metadata['secret_key'],415 token=metadata['token'],416 expiry_time=cls._expiry_datetime(metadata['expiry_time']),417 method=method,418 refresh_using=refresh_using,419 **kwargs,420 )421 return instance422 423 @property424 def access_key(self):425 """Warning: Using this property can lead to race conditions if you426 access another property subsequently along the refresh boundary.427 Please use get_frozen_credentials instead.428 """429 self._refresh()430 return self._access_key431 432 @access_key.setter433 def access_key(self, value):434 self._access_key = value435 436 @property437 def secret_key(self):438 """Warning: Using this property can lead to race conditions if you439 access another property subsequently along the refresh boundary.440 Please use get_frozen_credentials instead.441 """442 self._refresh()443 return self._secret_key444 445 @secret_key.setter446 def secret_key(self, value):447 self._secret_key = value448 449 @property450 def token(self):451 """Warning: Using this property can lead to race conditions if you452 access another property subsequently along the refresh boundary.453 Please use get_frozen_credentials instead.454 """455 self._refresh()456 return self._token457 458 @token.setter459 def token(self, value):460 self._token = value461 462 def _seconds_remaining(self):463 delta = self._expiry_time - self._time_fetcher()464 return total_seconds(delta)465 466 def refresh_needed(self, refresh_in=None):467 """Check if a refresh is needed.468 469 A refresh is needed if the expiry time associated470 with the temporary credentials is less than the471 provided ``refresh_in``. If ``time_delta`` is not472 provided, ``self.advisory_refresh_needed`` will be used.473 474 For example, if your temporary credentials expire475 in 10 minutes and the provided ``refresh_in`` is476 ``15 * 60``, then this function will return ``True``.477 478 :type refresh_in: int479 :param refresh_in: The number of seconds before the480 credentials expire in which refresh attempts should481 be made.482 483 :return: True if refresh needed, False otherwise.484 485 """486 if self._expiry_time is None:487 # No expiration, so assume we don't need to refresh.488 return False489 490 if refresh_in is None:491 refresh_in = self._advisory_refresh_timeout492 # The credentials should be refreshed if they're going to expire493 # in less than 5 minutes.494 if self._seconds_remaining() >= refresh_in:495 # There's enough time left. Don't refresh.496 return False497 logger.debug("Credentials need to be refreshed.")498 return True499 500 def _is_expired(self):501 # Checks if the current credentials are expired.502 return self.refresh_needed(refresh_in=0)503 504 def _refresh(self):505 # In the common case where we don't need a refresh, we506 # can immediately exit and not require acquiring the507 # refresh lock.508 if not self.refresh_needed(self._advisory_refresh_timeout):509 return510 511 # acquire() doesn't accept kwargs, but False is indicating512 # that we should not block if we can't acquire the lock.513 # If we aren't able to acquire the lock, we'll trigger514 # the else clause.515 if self._refresh_lock.acquire(False):516 try:517 if not self.refresh_needed(self._advisory_refresh_timeout):518 return519 is_mandatory_refresh = self.refresh_needed(520 self._mandatory_refresh_timeout521 )522 self._protected_refresh(is_mandatory=is_mandatory_refresh)523 return524 finally:525 self._refresh_lock.release()526 elif self.refresh_needed(self._mandatory_refresh_timeout):527 # If we're within the mandatory refresh window,528 # we must block until we get refreshed credentials.529 with self._refresh_lock:530 if not self.refresh_needed(self._mandatory_refresh_timeout):531 return532 self._protected_refresh(is_mandatory=True)533 534 def _protected_refresh(self, is_mandatory):535 # precondition: this method should only be called if you've acquired536 # the self._refresh_lock.537 try:538 metadata = self._refresh_using()539 except Exception:540 period_name = 'mandatory' if is_mandatory else 'advisory'541 logger.warning(542 "Refreshing temporary credentials failed "543 "during %s refresh period.",544 period_name,545 exc_info=True,546 )547 if is_mandatory:548 # If this is a mandatory refresh, then549 # all errors that occur when we attempt to refresh550 # credentials are propagated back to the user.551 raise552 # Otherwise we'll just return.553 # The end result will be that we'll use the current554 # set of temporary credentials we have.555 return556 self._set_from_data(metadata)557 self._frozen_credentials = ReadOnlyCredentials(558 self._access_key, self._secret_key, self._token559 )560 if self._is_expired():561 # We successfully refreshed credentials but for whatever562 # reason, our refreshing function returned credentials563 # that are still expired. In this scenario, the only564 # thing we can do is let the user know and raise565 # an exception.566 msg = (567 "Credentials were refreshed, but the "568 "refreshed credentials are still expired."569 )570 logger.warning(msg)571 raise RuntimeError(msg)572 573 @staticmethod574 def _expiry_datetime(time_str):575 return parse(time_str)576 577 def _set_from_data(self, data):578 expected_keys = ['access_key', 'secret_key', 'token', 'expiry_time']579 if not data:580 missing_keys = expected_keys581 else:582 missing_keys = [k for k in expected_keys if k not in data]583 584 if missing_keys:585 message = "Credential refresh failed, response did not contain: %s"586 raise CredentialRetrievalError(587 provider=self.method,588 error_msg=message % ', '.join(missing_keys),589 )590 591 self.access_key = data['access_key']592 self.secret_key = data['secret_key']593 self.token = data['token']594 self._expiry_time = parse(data['expiry_time'])595 logger.debug(596 "Retrieved credentials will expire at: %s", self._expiry_time597 )598 self._normalize()599 600 def get_frozen_credentials(self):601 """Return immutable credentials.602 603 The ``access_key``, ``secret_key``, and ``token`` properties604 on this class will always check and refresh credentials if605 needed before returning the particular credentials.606 607 This has an edge case where you can get inconsistent608 credentials. Imagine this:609 610 # Current creds are "t1"611 tmp.access_key ---> expired? no, so return t1.access_key612 # ---- time is now expired, creds need refreshing to "t2" ----613 tmp.secret_key ---> expired? yes, refresh and return t2.secret_key614 615 This means we're using the access key from t1 with the secret key616 from t2. To fix this issue, you can request a frozen credential object617 which is guaranteed not to change.618 619 The frozen credentials returned from this method should be used620 immediately and then discarded. The typical usage pattern would621 be::622 623 creds = RefreshableCredentials(...)624 some_code = SomeSignerObject()625 # I'm about to sign the request.626 # The frozen credentials are only used for the627 # duration of generate_presigned_url and will be628 # immediately thrown away.629 request = some_code.sign_some_request(630 with_credentials=creds.get_frozen_credentials())631 print("Signed request:", request)632 633 """634 self._refresh()635 return self._frozen_credentials636 637 638class DeferredRefreshableCredentials(RefreshableCredentials):639 """Refreshable credentials that don't require initial credentials.640 641 refresh_using will be called upon first access.642 """643 644 def __init__(self, refresh_using, method, time_fetcher=_local_now):645 self._refresh_using = refresh_using646 self._access_key = None647 self._secret_key = None648 self._token = None649 self._expiry_time = None650 self._time_fetcher = time_fetcher651 self._refresh_lock = threading.Lock()652 self.method = method653 self._frozen_credentials = None654 655 def refresh_needed(self, refresh_in=None):656 if self._frozen_credentials is None:657 return True658 return super().refresh_needed(refresh_in)659 660 661class CachedCredentialFetcher:662 DEFAULT_EXPIRY_WINDOW_SECONDS = 60 * 15663 664 def __init__(self, cache=None, expiry_window_seconds=None):665 if cache is None:666 cache = {}667 self._cache = cache668 self._cache_key = self._create_cache_key()669 if expiry_window_seconds is None:670 expiry_window_seconds = self.DEFAULT_EXPIRY_WINDOW_SECONDS671 self._expiry_window_seconds = expiry_window_seconds672 673 def _create_cache_key(self):674 raise NotImplementedError('_create_cache_key()')675 676 def _make_file_safe(self, filename):677 # Replace :, path sep, and / to make it the string filename safe.678 filename = filename.replace(':', '_').replace(os.sep, '_')679 return filename.replace('/', '_')680 681 def _get_credentials(self):682 raise NotImplementedError('_get_credentials()')683 684 def fetch_credentials(self):685 return self._get_cached_credentials()686 687 def _get_cached_credentials(self):688 """Get up-to-date credentials.689 690 This will check the cache for up-to-date credentials, calling assume691 role if none are available.692 """693 response = self._load_from_cache()694 if response is None:695 response = self._get_credentials()696 self._write_to_cache(response)697 else:698 logger.debug("Credentials for role retrieved from cache.")699 700 creds = response['Credentials']701 expiration = _serialize_if_needed(creds['Expiration'], iso=True)702 return {703 'access_key': creds['AccessKeyId'],704 'secret_key': creds['SecretAccessKey'],705 'token': creds['SessionToken'],706 'expiry_time': expiration,707 }708 709 def _load_from_cache(self):710 if self._cache_key in self._cache:711 creds = deepcopy(self._cache[self._cache_key])712 if not self._is_expired(creds):713 return creds714 else:715 logger.debug(716 "Credentials were found in cache, but they are expired."717 )718 return None719 720 def _write_to_cache(self, response):721 self._cache[self._cache_key] = deepcopy(response)722 723 def _is_expired(self, credentials):724 """Check if credentials are expired."""725 end_time = _parse_if_needed(credentials['Credentials']['Expiration'])726 seconds = total_seconds(end_time - _local_now())727 return seconds < self._expiry_window_seconds728 729 730class BaseAssumeRoleCredentialFetcher(CachedCredentialFetcher):731 def __init__(732 self,733 client_creator,734 role_arn,735 extra_args=None,736 cache=None,737 expiry_window_seconds=None,738 ):739 self._client_creator = client_creator740 self._role_arn = role_arn741 742 if extra_args is None:743 self._assume_kwargs = {}744 else:745 self._assume_kwargs = deepcopy(extra_args)746 self._assume_kwargs['RoleArn'] = self._role_arn747 748 self._role_session_name = self._assume_kwargs.get('RoleSessionName')749 self._using_default_session_name = False750 if not self._role_session_name:751 self._generate_assume_role_name()752 753 super().__init__(cache, expiry_window_seconds)754 755 def _generate_assume_role_name(self):756 self._role_session_name = 'botocore-session-%s' % (int(time.time()))757 self._assume_kwargs['RoleSessionName'] = self._role_session_name758 self._using_default_session_name = True759 760 def _create_cache_key(self):761 """Create a predictable cache key for the current configuration.762 763 The cache key is intended to be compatible with file names.764 """765 args = deepcopy(self._assume_kwargs)766 767 # The role session name gets randomly generated, so we don't want it768 # in the hash.769 if self._using_default_session_name:770 del args['RoleSessionName']771 772 if 'Policy' in args:773 # To have a predictable hash, the keys of the policy must be774 # sorted, so we have to load it here to make sure it gets sorted775 # later on.776 args['Policy'] = json.loads(args['Policy'])777 778 args = json.dumps(args, sort_keys=True)779 argument_hash = sha1(args.encode('utf-8')).hexdigest()780 return self._make_file_safe(argument_hash)781 782 783class AssumeRoleCredentialFetcher(BaseAssumeRoleCredentialFetcher):784 def __init__(785 self,786 client_creator,787 source_credentials,788 role_arn,789 extra_args=None,790 mfa_prompter=None,791 cache=None,792 expiry_window_seconds=None,793 ):794 """795 :type client_creator: callable796 :param client_creator: A callable that creates a client taking797 arguments like ``Session.create_client``.798 799 :type source_credentials: Credentials800 :param source_credentials: The credentials to use to create the801 client for the call to AssumeRole.802 803 :type role_arn: str804 :param role_arn: The ARN of the role to be assumed.805 806 :type extra_args: dict807 :param extra_args: Any additional arguments to add to the assume808 role request using the format of the botocore operation.809 Possible keys include, but may not be limited to,810 DurationSeconds, Policy, SerialNumber, ExternalId and811 RoleSessionName.812 813 :type mfa_prompter: callable814 :param mfa_prompter: A callable that returns input provided by the815 user (i.e raw_input, getpass.getpass, etc.).816 817 :type cache: dict818 :param cache: An object that supports ``__getitem__``,819 ``__setitem__``, and ``__contains__``. An example of this is820 the ``JSONFileCache`` class in aws-cli.821 822 :type expiry_window_seconds: int823 :param expiry_window_seconds: The amount of time, in seconds,824 """825 self._source_credentials = source_credentials826 self._mfa_prompter = mfa_prompter827 if self._mfa_prompter is None:828 self._mfa_prompter = getpass.getpass829 830 super().__init__(831 client_creator,832 role_arn,833 extra_args=extra_args,834 cache=cache,835 expiry_window_seconds=expiry_window_seconds,836 )837 838 def _get_credentials(self):839 """Get credentials by calling assume role."""840 kwargs = self._assume_role_kwargs()841 client = self._create_client()842 return client.assume_role(**kwargs)843 844 def _assume_role_kwargs(self):845 """Get the arguments for assume role based on current configuration."""846 assume_role_kwargs = deepcopy(self._assume_kwargs)847 848 mfa_serial = assume_role_kwargs.get('SerialNumber')849 850 if mfa_serial is not None:851 prompt = 'Enter MFA code for %s: ' % mfa_serial852 token_code = self._mfa_prompter(prompt)853 assume_role_kwargs['TokenCode'] = token_code854 855 duration_seconds = assume_role_kwargs.get('DurationSeconds')856 857 if duration_seconds is not None:858 assume_role_kwargs['DurationSeconds'] = duration_seconds859 860 return assume_role_kwargs861 862 def _create_client(self):863 """Create an STS client using the source credentials."""864 frozen_credentials = self._source_credentials.get_frozen_credentials()865 return self._client_creator(866 'sts',867 aws_access_key_id=frozen_credentials.access_key,868 aws_secret_access_key=frozen_credentials.secret_key,869 aws_session_token=frozen_credentials.token,870 )871 872 873class AssumeRoleWithWebIdentityCredentialFetcher(874 BaseAssumeRoleCredentialFetcher875):876 def __init__(877 self,878 client_creator,879 web_identity_token_loader,880 role_arn,881 extra_args=None,882 cache=None,883 expiry_window_seconds=None,884 ):885 """886 :type client_creator: callable887 :param client_creator: A callable that creates a client taking888 arguments like ``Session.create_client``.889 890 :type web_identity_token_loader: callable891 :param web_identity_token_loader: A callable that takes no arguments892 and returns a web identity token str.893 894 :type role_arn: str895 :param role_arn: The ARN of the role to be assumed.896 897 :type extra_args: dict898 :param extra_args: Any additional arguments to add to the assume899 role request using the format of the botocore operation.900 Possible keys include, but may not be limited to,901 DurationSeconds, Policy, SerialNumber, ExternalId and902 RoleSessionName.903 904 :type cache: dict905 :param cache: An object that supports ``__getitem__``,906 ``__setitem__``, and ``__contains__``. An example of this is907 the ``JSONFileCache`` class in aws-cli.908 909 :type expiry_window_seconds: int910 :param expiry_window_seconds: The amount of time, in seconds,911 """912 self._web_identity_token_loader = web_identity_token_loader913 914 super().__init__(915 client_creator,916 role_arn,917 extra_args=extra_args,918 cache=cache,919 expiry_window_seconds=expiry_window_seconds,920 )921 922 def _get_credentials(self):923 """Get credentials by calling assume role."""924 kwargs = self._assume_role_kwargs()925 # Assume role with web identity does not require credentials other than926 # the token, explicitly configure the client to not sign requests.927 config = Config(signature_version=UNSIGNED)928 client = self._client_creator('sts', config=config)929 return client.assume_role_with_web_identity(**kwargs)930 931 def _assume_role_kwargs(self):932 """Get the arguments for assume role based on current configuration."""933 assume_role_kwargs = deepcopy(self._assume_kwargs)934 identity_token = self._web_identity_token_loader()935 assume_role_kwargs['WebIdentityToken'] = identity_token936 937 return assume_role_kwargs938 939 940class CredentialProvider:941 # A short name to identify the provider within botocore.942 METHOD = None943 944 # A name to identify the provider for use in cross-sdk features like945 # assume role's `credential_source` configuration option. These names946 # are to be treated in a case-insensitive way. NOTE: any providers not947 # implemented in botocore MUST prefix their canonical names with948 # 'custom' or we DO NOT guarantee that it will work with any features949 # that this provides.950 CANONICAL_NAME = None951 952 def __init__(self, session=None):953 self.session = session954 955 def load(self):956 """957 Loads the credentials from their source & sets them on the object.958 959 Subclasses should implement this method (by reading from disk, the960 environment, the network or wherever), returning ``True`` if they were961 found & loaded.962 963 If not found, this method should return ``False``, indictating that the964 ``CredentialResolver`` should fall back to the next available method.965 966 The default implementation does nothing, assuming the user has set the967 ``access_key/secret_key/token`` themselves.968 969 :returns: Whether credentials were found & set970 :rtype: Credentials971 """972 return True973 974 def _extract_creds_from_mapping(self, mapping, *key_names):975 found = []976 for key_name in key_names:977 try:978 found.append(mapping[key_name])979 except KeyError:980 raise PartialCredentialsError(981 provider=self.METHOD, cred_var=key_name982 )983 return found984 985 986class ProcessProvider(CredentialProvider):987 METHOD = 'custom-process'988 989 def __init__(self, profile_name, load_config, popen=subprocess.Popen):990 self._profile_name = profile_name991 self._load_config = load_config992 self._loaded_config = None993 self._popen = popen994 995 def load(self):996 credential_process = self._credential_process997 if credential_process is None:998 return999 1000 creds_dict = self._retrieve_credentials_using(credential_process)1001 if creds_dict.get('expiry_time') is not None:1002 return RefreshableCredentials.create_from_metadata(1003 creds_dict,1004 lambda: self._retrieve_credentials_using(credential_process),1005 self.METHOD,1006 )1007 1008 return Credentials(1009 access_key=creds_dict['access_key'],1010 secret_key=creds_dict['secret_key'],1011 token=creds_dict.get('token'),1012 method=self.METHOD,1013 )1014 1015 def _retrieve_credentials_using(self, credential_process):1016 # We're not using shell=True, so we need to pass the1017 # command and all arguments as a list.1018 process_list = compat_shell_split(credential_process)1019 p = self._popen(1020 process_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE1021 )1022 stdout, stderr = p.communicate()1023 if p.returncode != 0:1024 raise CredentialRetrievalError(1025 provider=self.METHOD, error_msg=stderr.decode('utf-8')1026 )1027 parsed = botocore.compat.json.loads(stdout.decode('utf-8'))1028 version = parsed.get('Version', '<Version key not provided>')1029 if version != 1:1030 raise CredentialRetrievalError(1031 provider=self.METHOD,1032 error_msg=(1033 f"Unsupported version '{version}' for credential process "1034 f"provider, supported versions: 1"1035 ),1036 )1037 try:1038 return {1039 'access_key': parsed['AccessKeyId'],1040 'secret_key': parsed['SecretAccessKey'],1041 'token': parsed.get('SessionToken'),1042 'expiry_time': parsed.get('Expiration'),1043 }1044 except KeyError as e:1045 raise CredentialRetrievalError(1046 provider=self.METHOD,1047 error_msg=f"Missing required key in response: {e}",1048 )1049 1050 @property1051 def _credential_process(self):1052 if self._loaded_config is None:1053 self._loaded_config = self._load_config()1054 profile_config = self._loaded_config.get('profiles', {}).get(1055 self._profile_name, {}1056 )1057 return profile_config.get('credential_process')1058 1059 1060class InstanceMetadataProvider(CredentialProvider):1061 METHOD = 'iam-role'1062 CANONICAL_NAME = 'Ec2InstanceMetadata'1063 1064 def __init__(self, iam_role_fetcher):1065 self._role_fetcher = iam_role_fetcher1066 1067 def load(self):1068 fetcher = self._role_fetcher1069 # We do the first request, to see if we get useful data back.1070 # If not, we'll pass & move on to whatever's next in the credential1071 # chain.1072 metadata = fetcher.retrieve_iam_role_credentials()1073 if not metadata:1074 return None1075 logger.info(1076 'Found credentials from IAM Role: %s', metadata['role_name']1077 )1078 # We manually set the data here, since we already made the request &1079 # have it. When the expiry is hit, the credentials will auto-refresh1080 # themselves.1081 creds = RefreshableCredentials.create_from_metadata(1082 metadata,1083 method=self.METHOD,1084 refresh_using=fetcher.retrieve_iam_role_credentials,1085 )1086 return creds1087 1088 1089class EnvProvider(CredentialProvider):1090 METHOD = 'env'1091 CANONICAL_NAME = 'Environment'1092 ACCESS_KEY = 'AWS_ACCESS_KEY_ID'1093 SECRET_KEY = 'AWS_SECRET_ACCESS_KEY'1094 # The token can come from either of these env var.1095 # AWS_SESSION_TOKEN is what other AWS SDKs have standardized on.1096 TOKENS = ['AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN']1097 EXPIRY_TIME = 'AWS_CREDENTIAL_EXPIRATION'1098 1099 def __init__(self, environ=None, mapping=None):1100 """1101 1102 :param environ: The environment variables (defaults to1103 ``os.environ`` if no value is provided).1104 :param mapping: An optional mapping of variable names to1105 environment variable names. Use this if you want to1106 change the mapping of access_key->AWS_ACCESS_KEY_ID, etc.1107 The dict can have up to 3 keys: ``access_key``, ``secret_key``,1108 ``session_token``.1109 """1110 if environ is None:1111 environ = os.environ1112 self.environ = environ1113 self._mapping = self._build_mapping(mapping)1114 1115 def _build_mapping(self, mapping):1116 # Mapping of variable name to env var name.1117 var_mapping = {}1118 if mapping is None:1119 # Use the class var default.1120 var_mapping['access_key'] = self.ACCESS_KEY1121 var_mapping['secret_key'] = self.SECRET_KEY1122 var_mapping['token'] = self.TOKENS1123 var_mapping['expiry_time'] = self.EXPIRY_TIME1124 else:1125 var_mapping['access_key'] = mapping.get(1126 'access_key', self.ACCESS_KEY1127 )1128 var_mapping['secret_key'] = mapping.get(1129 'secret_key', self.SECRET_KEY1130 )1131 var_mapping['token'] = mapping.get('token', self.TOKENS)1132 if not isinstance(var_mapping['token'], list):1133 var_mapping['token'] = [var_mapping['token']]1134 var_mapping['expiry_time'] = mapping.get(1135 'expiry_time', self.EXPIRY_TIME1136 )1137 return var_mapping1138 1139 def load(self):1140 """1141 Search for credentials in explicit environment variables.1142 """1143 1144 access_key = self.environ.get(self._mapping['access_key'], '')1145 1146 if access_key:1147 logger.info('Found credentials in environment variables.')1148 fetcher = self._create_credentials_fetcher()1149 credentials = fetcher(require_expiry=False)1150 1151 expiry_time = credentials['expiry_time']1152 if expiry_time is not None:1153 expiry_time = parse(expiry_time)1154 return RefreshableCredentials(1155 credentials['access_key'],1156 credentials['secret_key'],1157 credentials['token'],1158 expiry_time,1159 refresh_using=fetcher,1160 method=self.METHOD,1161 )1162 1163 return Credentials(1164 credentials['access_key'],1165 credentials['secret_key'],1166 credentials['token'],1167 method=self.METHOD,1168 )1169 else:1170 return None1171 1172 def _create_credentials_fetcher(self):1173 mapping = self._mapping1174 method = self.METHOD1175 environ = self.environ1176 1177 def fetch_credentials(require_expiry=True):1178 credentials = {}1179 1180 access_key = environ.get(mapping['access_key'], '')1181 if not access_key:1182 raise PartialCredentialsError(1183 provider=method, cred_var=mapping['access_key']1184 )1185 credentials['access_key'] = access_key1186 1187 secret_key = environ.get(mapping['secret_key'], '')1188 if not secret_key:1189 raise PartialCredentialsError(1190 provider=method, cred_var=mapping['secret_key']1191 )1192 credentials['secret_key'] = secret_key1193 1194 credentials['token'] = None1195 for token_env_var in mapping['token']:1196 token = environ.get(token_env_var, '')1197 if token:1198 credentials['token'] = token1199 break1200 