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"). You 5# may not use this file except in compliance with the License. A copy of 6# the License is located at 7# 8# http://aws.amazon.com/apache2.0/ 9# 10# or in the "license" file accompanying this file. This file is 11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 12# ANY KIND, either express or implied. See the License for the specific 13# language governing permissions and limitations under the License. 14import time 15import datetime 16import logging 17import os 18import getpass 19import threading 20import json 21import subprocess 22from collections import namedtuple 23from copy import deepcopy 24from hashlib import sha1 25 26from dateutil.parser import parse 27from dateutil.tz import tzlocal, tzutc 28 29import botocore.configloader 30import botocore.compat 31from botocore import UNSIGNED 32from botocore.compat import total_seconds 33from botocore.compat import compat_shell_split 34from botocore.config import Config 35from botocore.exceptions import UnknownCredentialError 36from botocore.exceptions import PartialCredentialsError 37from botocore.exceptions import ConfigNotFound 38from botocore.exceptions import InvalidConfigError 39from botocore.exceptions import InfiniteLoopConfigError 40from botocore.exceptions import RefreshWithMFAUnsupportedError 41from botocore.exceptions import MetadataRetrievalError 42from botocore.exceptions import CredentialRetrievalError 43from botocore.exceptions import UnauthorizedSSOTokenError 44from botocore.utils import InstanceMetadataFetcher, parse_key_val_file 45from botocore.utils import ContainerMetadataFetcher 46from botocore.utils import FileWebIdentityTokenLoader 47from botocore.utils import SSOTokenLoader 48 49 50logger = logging.getLogger(__name__) 51ReadOnlyCredentials = namedtuple('ReadOnlyCredentials', 52 ['access_key', 'secret_key', 'token']) 53 54 55def create_credential_resolver(session, cache=None, region_name=None): 56 """Create a default credential resolver. 57 58 This creates a pre-configured credential resolver 59 that includes the default lookup chain for 60 credentials. 61 62 """ 63 profile_name = session.get_config_variable('profile') or 'default' 64 metadata_timeout = session.get_config_variable('metadata_service_timeout') 65 num_attempts = session.get_config_variable('metadata_service_num_attempts') 66 disable_env_vars = session.instance_variables().get('profile') is not None 67 68 if cache is None: 69 cache = {} 70 71 env_provider = EnvProvider() 72 container_provider = ContainerProvider() 73 instance_metadata_provider = InstanceMetadataProvider( 74 iam_role_fetcher=InstanceMetadataFetcher( 75 timeout=metadata_timeout, 76 num_attempts=num_attempts, 77 user_agent=session.user_agent()) 78 ) 79 80 profile_provider_builder = ProfileProviderBuilder( 81 session, cache=cache, region_name=region_name) 82 assume_role_provider = AssumeRoleProvider( 83 load_config=lambda: session.full_config, 84 client_creator=_get_client_creator(session, region_name), 85 cache=cache, 86 profile_name=profile_name, 87 credential_sourcer=CanonicalNameCredentialSourcer([ 88 env_provider, container_provider, instance_metadata_provider 89 ]), 90 profile_provider_builder=profile_provider_builder, 91 ) 92 93 pre_profile = [ 94 env_provider, 95 assume_role_provider, 96 ] 97 profile_providers = profile_provider_builder.providers( 98 profile_name=profile_name, 99 disable_env_vars=disable_env_vars, 100 ) 101 post_profile = [ 102 OriginalEC2Provider(), 103 BotoProvider(), 104 container_provider, 105 instance_metadata_provider, 106 ] 107 providers = pre_profile + profile_providers + post_profile 108 109 if disable_env_vars: 110 # An explicitly provided profile will negate an EnvProvider. 111 # We will defer to providers that understand the "profile" 112 # concept to retrieve credentials. 113 # The one edge case if is all three values are provided via 114 # env vars: 115 # export AWS_ACCESS_KEY_ID=foo 116 # export AWS_SECRET_ACCESS_KEY=bar 117 # export AWS_PROFILE=baz 118 # Then, just like our client() calls, the explicit credentials 119 # will take precedence. 120 # 121 # This precedence is enforced by leaving the EnvProvider in the chain. 122 # This means that the only way a "profile" would win is if the 123 # EnvProvider does not return credentials, which is what we want 124 # in this scenario. 125 providers.remove(env_provider) 126 logger.debug('Skipping environment variable credential check' 127 ' because profile name was explicitly set.') 128 129 resolver = CredentialResolver(providers=providers) 130 return resolver 131 132 133class ProfileProviderBuilder(object): 134 """This class handles the creation of profile based providers. 135 136 NOTE: This class is only intended for internal use. 137 138 This class handles the creation and ordering of the various credential 139 providers that primarly source their configuration from the shared config. 140 This is needed to enable sharing between the default credential chain and 141 the source profile chain created by the assume role provider. 142 """ 143 def __init__(self, session, cache=None, region_name=None, 144 sso_token_cache=None): 145 self._session = session 146 self._cache = cache 147 self._region_name = region_name 148 self._sso_token_cache = sso_token_cache 149 150 def providers(self, profile_name, disable_env_vars=False): 151 return [ 152 self._create_web_identity_provider( 153 profile_name, disable_env_vars, 154 ), 155 self._create_sso_provider(profile_name), 156 self._create_shared_credential_provider(profile_name), 157 self._create_process_provider(profile_name), 158 self._create_config_provider(profile_name), 159 ] 160 161 def _create_process_provider(self, profile_name): 162 return ProcessProvider( 163 profile_name=profile_name, 164 load_config=lambda: self._session.full_config, 165 ) 166 167 def _create_shared_credential_provider(self, profile_name): 168 credential_file = self._session.get_config_variable('credentials_file') 169 return SharedCredentialProvider( 170 profile_name=profile_name, 171 creds_filename=credential_file, 172 ) 173 174 def _create_config_provider(self, profile_name): 175 config_file = self._session.get_config_variable('config_file') 176 return ConfigProvider( 177 profile_name=profile_name, 178 config_filename=config_file, 179 ) 180 181 def _create_web_identity_provider(self, profile_name, disable_env_vars): 182 return AssumeRoleWithWebIdentityProvider( 183 load_config=lambda: self._session.full_config, 184 client_creator=_get_client_creator( 185 self._session, self._region_name), 186 cache=self._cache, 187 profile_name=profile_name, 188 disable_env_vars=disable_env_vars, 189 ) 190 191 def _create_sso_provider(self, profile_name): 192 return SSOProvider( 193 load_config=lambda: self._session.full_config, 194 client_creator=self._session.create_client, 195 profile_name=profile_name, 196 cache=self._cache, 197 token_cache=self._sso_token_cache, 198 ) 199 200 201def get_credentials(session): 202 resolver = create_credential_resolver(session) 203 return resolver.load_credentials() 204 205 206def _local_now(): 207 return datetime.datetime.now(tzlocal()) 208 209 210def _parse_if_needed(value): 211 if isinstance(value, datetime.datetime): 212 return value 213 return parse(value) 214 215 216def _serialize_if_needed(value, iso=False): 217 if isinstance(value, datetime.datetime): 218 if iso: 219 return value.isoformat() 220 return value.strftime('%Y-%m-%dT%H:%M:%S%Z') 221 return value 222 223 224def _get_client_creator(session, region_name): 225 def client_creator(service_name, **kwargs): 226 create_client_kwargs = { 227 'region_name': region_name 228 } 229 create_client_kwargs.update(**kwargs) 230 return session.create_client(service_name, **create_client_kwargs) 231 232 return client_creator 233 234 235def create_assume_role_refresher(client, params): 236 def refresh(): 237 response = client.assume_role(**params) 238 credentials = response['Credentials'] 239 # We need to normalize the credential names to 240 # the values expected by the refresh creds. 241 return { 242 'access_key': credentials['AccessKeyId'], 243 'secret_key': credentials['SecretAccessKey'], 244 'token': credentials['SessionToken'], 245 'expiry_time': _serialize_if_needed(credentials['Expiration']), 246 } 247 return refresh 248 249 250def create_mfa_serial_refresher(actual_refresh): 251 252 class _Refresher(object): 253 def __init__(self, refresh): 254 self._refresh = refresh 255 self._has_been_called = False 256 257 def __call__(self): 258 if self._has_been_called: 259 # We can explore an option in the future to support 260 # reprompting for MFA, but for now we just error out 261 # when the temp creds expire. 262 raise RefreshWithMFAUnsupportedError() 263 self._has_been_called = True 264 return self._refresh() 265 266 return _Refresher(actual_refresh) 267 268 269class JSONFileCache(object): 270 """JSON file cache. 271 This provides a dict like interface that stores JSON serializable 272 objects. 273 The objects are serialized to JSON and stored in a file. These 274 values can be retrieved at a later time. 275 """ 276 277 CACHE_DIR = os.path.expanduser(os.path.join('~', '.aws', 'boto', 'cache')) 278 279 def __init__(self, working_dir=CACHE_DIR): 280 self._working_dir = working_dir 281 282 def __contains__(self, cache_key): 283 actual_key = self._convert_cache_key(cache_key) 284 return os.path.isfile(actual_key) 285 286 def __getitem__(self, cache_key): 287 """Retrieve value from a cache key.""" 288 actual_key = self._convert_cache_key(cache_key) 289 try: 290 with open(actual_key) as f: 291 return json.load(f) 292 except (OSError, ValueError, IOError): 293 raise KeyError(cache_key) 294 295 def __setitem__(self, cache_key, value): 296 full_key = self._convert_cache_key(cache_key) 297 try: 298 file_content = json.dumps(value, default=_serialize_if_needed) 299 except (TypeError, ValueError): 300 raise ValueError("Value cannot be cached, must be " 301 "JSON serializable: %s" % value) 302 if not os.path.isdir(self._working_dir): 303 os.makedirs(self._working_dir) 304 with os.fdopen(os.open(full_key, 305 os.O_WRONLY | os.O_CREAT, 0o600), 'w') as f: 306 f.truncate() 307 f.write(file_content) 308 309 def _convert_cache_key(self, cache_key): 310 full_path = os.path.join(self._working_dir, cache_key + '.json') 311 return full_path 312 313 314class Credentials(object): 315 """ 316 Holds the credentials needed to authenticate requests. 317 318 :ivar access_key: The access key part of the credentials. 319 :ivar secret_key: The secret key part of the credentials. 320 :ivar token: The security token, valid only for session credentials. 321 :ivar method: A string which identifies where the credentials 322 were found. 323 """ 324 325 def __init__(self, access_key, secret_key, token=None, 326 method=None): 327 self.access_key = access_key 328 self.secret_key = secret_key 329 self.token = token 330 331 if method is None: 332 method = 'explicit' 333 self.method = method 334 335 self._normalize() 336 337 def _normalize(self): 338 # Keys would sometimes (accidentally) contain non-ascii characters. 339 # It would cause a confusing UnicodeDecodeError in Python 2. 340 # We explicitly convert them into unicode to avoid such error. 341 # 342 # Eventually the service will decide whether to accept the credential. 343 # This also complies with the behavior in Python 3. 344 self.access_key = botocore.compat.ensure_unicode(self.access_key) 345 self.secret_key = botocore.compat.ensure_unicode(self.secret_key) 346 347 def get_frozen_credentials(self): 348 return ReadOnlyCredentials(self.access_key, 349 self.secret_key, 350 self.token) 351 352 353class RefreshableCredentials(Credentials): 354 """ 355 Holds the credentials needed to authenticate requests. In addition, it 356 knows how to refresh itself. 357 358 :ivar access_key: The access key part of the credentials. 359 :ivar secret_key: The secret key part of the credentials. 360 :ivar token: The security token, valid only for session credentials. 361 :ivar method: A string which identifies where the credentials 362 were found. 363 """ 364 # The time at which we'll attempt to refresh, but not 365 # block if someone else is refreshing. 366 _advisory_refresh_timeout = 15 * 60 367 # The time at which all threads will block waiting for 368 # refreshed credentials. 369 _mandatory_refresh_timeout = 10 * 60 370 371 def __init__(self, access_key, secret_key, token, 372 expiry_time, refresh_using, method, 373 time_fetcher=_local_now): 374 self._refresh_using = refresh_using 375 self._access_key = access_key 376 self._secret_key = secret_key 377 self._token = token 378 self._expiry_time = expiry_time 379 self._time_fetcher = time_fetcher 380 self._refresh_lock = threading.Lock() 381 self.method = method 382 self._frozen_credentials = ReadOnlyCredentials( 383 access_key, secret_key, token) 384 self._normalize() 385 386 def _normalize(self): 387 self._access_key = botocore.compat.ensure_unicode(self._access_key) 388 self._secret_key = botocore.compat.ensure_unicode(self._secret_key) 389 390 @classmethod 391 def create_from_metadata(cls, metadata, refresh_using, method): 392 instance = cls( 393 access_key=metadata['access_key'], 394 secret_key=metadata['secret_key'], 395 token=metadata['token'], 396 expiry_time=cls._expiry_datetime(metadata['expiry_time']), 397 method=method, 398 refresh_using=refresh_using 399 ) 400 return instance 401 402 @property 403 def access_key(self): 404 """Warning: Using this property can lead to race conditions if you 405 access another property subsequently along the refresh boundary. 406 Please use get_frozen_credentials instead. 407 """ 408 self._refresh() 409 return self._access_key 410 411 @access_key.setter 412 def access_key(self, value): 413 self._access_key = value 414 415 @property 416 def secret_key(self): 417 """Warning: Using this property can lead to race conditions if you 418 access another property subsequently along the refresh boundary. 419 Please use get_frozen_credentials instead. 420 """ 421 self._refresh() 422 return self._secret_key 423 424 @secret_key.setter 425 def secret_key(self, value): 426 self._secret_key = value 427 428 @property 429 def token(self): 430 """Warning: Using this property can lead to race conditions if you 431 access another property subsequently along the refresh boundary. 432 Please use get_frozen_credentials instead. 433 """ 434 self._refresh() 435 return self._token 436 437 @token.setter 438 def token(self, value): 439 self._token = value 440 441 def _seconds_remaining(self): 442 delta = self._expiry_time - self._time_fetcher() 443 return total_seconds(delta) 444 445 def refresh_needed(self, refresh_in=None): 446 """Check if a refresh is needed. 447 448 A refresh is needed if the expiry time associated 449 with the temporary credentials is less than the 450 provided ``refresh_in``. If ``time_delta`` is not 451 provided, ``self.advisory_refresh_needed`` will be used. 452 453 For example, if your temporary credentials expire 454 in 10 minutes and the provided ``refresh_in`` is 455 ``15 * 60``, then this function will return ``True``. 456 457 :type refresh_in: int 458 :param refresh_in: The number of seconds before the 459 credentials expire in which refresh attempts should 460 be made. 461 462 :return: True if refresh needed, False otherwise. 463 464 """ 465 if self._expiry_time is None: 466 # No expiration, so assume we don't need to refresh. 467 return False 468 469 if refresh_in is None: 470 refresh_in = self._advisory_refresh_timeout 471 # The credentials should be refreshed if they're going to expire 472 # in less than 5 minutes. 473 if self._seconds_remaining() >= refresh_in: 474 # There's enough time left. Don't refresh. 475 return False 476 logger.debug("Credentials need to be refreshed.") 477 return True 478 479 def _is_expired(self): 480 # Checks if the current credentials are expired. 481 return self.refresh_needed(refresh_in=0) 482 483 def _refresh(self): 484 # In the common case where we don't need a refresh, we 485 # can immediately exit and not require acquiring the 486 # refresh lock. 487 if not self.refresh_needed(self._advisory_refresh_timeout): 488 return 489 490 # acquire() doesn't accept kwargs, but False is indicating 491 # that we should not block if we can't acquire the lock. 492 # If we aren't able to acquire the lock, we'll trigger 493 # the else clause. 494 if self._refresh_lock.acquire(False): 495 try: 496 if not self.refresh_needed(self._advisory_refresh_timeout): 497 return 498 is_mandatory_refresh = self.refresh_needed( 499 self._mandatory_refresh_timeout) 500 self._protected_refresh(is_mandatory=is_mandatory_refresh) 501 return 502 finally: 503 self._refresh_lock.release() 504 elif self.refresh_needed(self._mandatory_refresh_timeout): 505 # If we're within the mandatory refresh window, 506 # we must block until we get refreshed credentials. 507 with self._refresh_lock: 508 if not self.refresh_needed(self._mandatory_refresh_timeout): 509 return 510 self._protected_refresh(is_mandatory=True) 511 512 def _protected_refresh(self, is_mandatory): 513 # precondition: this method should only be called if you've acquired 514 # the self._refresh_lock. 515 try: 516 metadata = self._refresh_using() 517 except Exception as e: 518 period_name = 'mandatory' if is_mandatory else 'advisory' 519 logger.warning("Refreshing temporary credentials failed " 520 "during %s refresh period.", 521 period_name, exc_info=True) 522 if is_mandatory: 523 # If this is a mandatory refresh, then 524 # all errors that occur when we attempt to refresh 525 # credentials are propagated back to the user. 526 raise 527 # Otherwise we'll just return. 528 # The end result will be that we'll use the current 529 # set of temporary credentials we have. 530 return 531 self._set_from_data(metadata) 532 self._frozen_credentials = ReadOnlyCredentials( 533 self._access_key, self._secret_key, self._token) 534 if self._is_expired(): 535 # We successfully refreshed credentials but for whatever 536 # reason, our refreshing function returned credentials 537 # that are still expired. In this scenario, the only 538 # thing we can do is let the user know and raise 539 # an exception. 540 msg = ("Credentials were refreshed, but the " 541 "refreshed credentials are still expired.") 542 logger.warning(msg) 543 raise RuntimeError(msg) 544 545 @staticmethod 546 def _expiry_datetime(time_str): 547 return parse(time_str) 548 549 def _set_from_data(self, data): 550 expected_keys = ['access_key', 'secret_key', 'token', 'expiry_time'] 551 if not data: 552 missing_keys = expected_keys 553 else: 554 missing_keys = [k for k in expected_keys if k not in data] 555 556 if missing_keys: 557 message = "Credential refresh failed, response did not contain: %s" 558 raise CredentialRetrievalError( 559 provider=self.method, 560 error_msg=message % ', '.join(missing_keys), 561 ) 562 563 self.access_key = data['access_key'] 564 self.secret_key = data['secret_key'] 565 self.token = data['token'] 566 self._expiry_time = parse(data['expiry_time']) 567 logger.debug("Retrieved credentials will expire at: %s", 568 self._expiry_time) 569 self._normalize() 570 571 def get_frozen_credentials(self): 572 """Return immutable credentials. 573 574 The ``access_key``, ``secret_key``, and ``token`` properties 575 on this class will always check and refresh credentials if 576 needed before returning the particular credentials. 577 578 This has an edge case where you can get inconsistent 579 credentials. Imagine this: 580 581 # Current creds are "t1" 582 tmp.access_key ---> expired? no, so return t1.access_key 583 # ---- time is now expired, creds need refreshing to "t2" ---- 584 tmp.secret_key ---> expired? yes, refresh and return t2.secret_key 585 586 This means we're using the access key from t1 with the secret key 587 from t2. To fix this issue, you can request a frozen credential object 588 which is guaranteed not to change. 589 590 The frozen credentials returned from this method should be used 591 immediately and then discarded. The typical usage pattern would 592 be:: 593 594 creds = RefreshableCredentials(...) 595 some_code = SomeSignerObject() 596 # I'm about to sign the request. 597 # The frozen credentials are only used for the 598 # duration of generate_presigned_url and will be 599 # immediately thrown away. 600 request = some_code.sign_some_request( 601 with_credentials=creds.get_frozen_credentials()) 602 print("Signed request:", request) 603 604 """ 605 self._refresh() 606 return self._frozen_credentials 607 608 609class DeferredRefreshableCredentials(RefreshableCredentials): 610 """Refreshable credentials that don't require initial credentials. 611 612 refresh_using will be called upon first access. 613 """ 614 def __init__(self, refresh_using, method, time_fetcher=_local_now): 615 self._refresh_using = refresh_using 616 self._access_key = None 617 self._secret_key = None 618 self._token = None 619 self._expiry_time = None 620 self._time_fetcher = time_fetcher 621 self._refresh_lock = threading.Lock() 622 self.method = method 623 self._frozen_credentials = None 624 625 def refresh_needed(self, refresh_in=None): 626 if self._frozen_credentials is None: 627 return True 628 return super(DeferredRefreshableCredentials, self).refresh_needed( 629 refresh_in 630 ) 631 632 633class CachedCredentialFetcher(object): 634 DEFAULT_EXPIRY_WINDOW_SECONDS = 60 * 15 635 636 def __init__(self, cache=None, expiry_window_seconds=None): 637 if cache is None: 638 cache = {} 639 self._cache = cache 640 self._cache_key = self._create_cache_key() 641 if expiry_window_seconds is None: 642 expiry_window_seconds = self.DEFAULT_EXPIRY_WINDOW_SECONDS 643 self._expiry_window_seconds = expiry_window_seconds 644 645 def _create_cache_key(self): 646 raise NotImplementedError('_create_cache_key()') 647 648 def _make_file_safe(self, filename): 649 # Replace :, path sep, and / to make it the string filename safe. 650 filename = filename.replace(':', '_').replace(os.path.sep, '_') 651 return filename.replace('/', '_') 652 653 def _get_credentials(self): 654 raise NotImplementedError('_get_credentials()') 655 656 def fetch_credentials(self): 657 return self._get_cached_credentials() 658 659 def _get_cached_credentials(self): 660 """Get up-to-date credentials. 661 662 This will check the cache for up-to-date credentials, calling assume 663 role if none are available. 664 """ 665 response = self._load_from_cache() 666 if response is None: 667 response = self._get_credentials() 668 self._write_to_cache(response) 669 else: 670 logger.debug("Credentials for role retrieved from cache.") 671 672 creds = response['Credentials'] 673 expiration = _serialize_if_needed(creds['Expiration'], iso=True) 674 return { 675 'access_key': creds['AccessKeyId'], 676 'secret_key': creds['SecretAccessKey'], 677 'token': creds['SessionToken'], 678 'expiry_time': expiration, 679 } 680 681 def _load_from_cache(self): 682 if self._cache_key in self._cache: 683 creds = deepcopy(self._cache[self._cache_key]) 684 if not self._is_expired(creds): 685 return creds 686 else: 687 logger.debug( 688 "Credentials were found in cache, but they are expired." 689 ) 690 return None 691 692 def _write_to_cache(self, response): 693 self._cache[self._cache_key] = deepcopy(response) 694 695 def _is_expired(self, credentials): 696 """Check if credentials are expired.""" 697 end_time = _parse_if_needed(credentials['Credentials']['Expiration']) 698 seconds = total_seconds(end_time - _local_now()) 699 return seconds < self._expiry_window_seconds 700 701 702class BaseAssumeRoleCredentialFetcher(CachedCredentialFetcher): 703 def __init__(self, client_creator, role_arn, extra_args=None, 704 cache=None, expiry_window_seconds=None): 705 self._client_creator = client_creator 706 self._role_arn = role_arn 707 708 if extra_args is None: 709 self._assume_kwargs = {} 710 else: 711 self._assume_kwargs = deepcopy(extra_args) 712 self._assume_kwargs['RoleArn'] = self._role_arn 713 714 self._role_session_name = self._assume_kwargs.get('RoleSessionName') 715 self._using_default_session_name = False 716 if not self._role_session_name: 717 self._generate_assume_role_name() 718 719 super(BaseAssumeRoleCredentialFetcher, self).__init__( 720 cache, expiry_window_seconds 721 ) 722 723 def _generate_assume_role_name(self): 724 self._role_session_name = 'botocore-session-%s' % (int(time.time())) 725 self._assume_kwargs['RoleSessionName'] = self._role_session_name 726 self._using_default_session_name = True 727 728 def _create_cache_key(self): 729 """Create a predictable cache key for the current configuration. 730 731 The cache key is intended to be compatible with file names. 732 """ 733 args = deepcopy(self._assume_kwargs) 734 735 # The role session name gets randomly generated, so we don't want it 736 # in the hash. 737 if self._using_default_session_name: 738 del args['RoleSessionName'] 739 740 if 'Policy' in args: 741 # To have a predictable hash, the keys of the policy must be 742 # sorted, so we have to load it here to make sure it gets sorted 743 # later on. 744 args['Policy'] = json.loads(args['Policy']) 745 746 args = json.dumps(args, sort_keys=True) 747 argument_hash = sha1(args.encode('utf-8')).hexdigest() 748 return self._make_file_safe(argument_hash) 749 750 751class AssumeRoleCredentialFetcher(BaseAssumeRoleCredentialFetcher): 752 def __init__(self, client_creator, source_credentials, role_arn, 753 extra_args=None, mfa_prompter=None, cache=None, 754 expiry_window_seconds=None): 755 """ 756 :type client_creator: callable 757 :param client_creator: A callable that creates a client taking 758 arguments like ``Session.create_client``. 759 760 :type source_credentials: Credentials 761 :param source_credentials: The credentials to use to create the 762 client for the call to AssumeRole. 763 764 :type role_arn: str 765 :param role_arn: The ARN of the role to be assumed. 766 767 :type extra_args: dict 768 :param extra_args: Any additional arguments to add to the assume 769 role request using the format of the botocore operation. 770 Possible keys include, but may not be limited to, 771 DurationSeconds, Policy, SerialNumber, ExternalId and 772 RoleSessionName. 773 774 :type mfa_prompter: callable 775 :param mfa_prompter: A callable that returns input provided by the 776 user (i.e raw_input, getpass.getpass, etc.). 777 778 :type cache: dict 779 :param cache: An object that supports ``__getitem__``, 780 ``__setitem__``, and ``__contains__``. An example of this is 781 the ``JSONFileCache`` class in aws-cli. 782 783 :type expiry_window_seconds: int 784 :param expiry_window_seconds: The amount of time, in seconds, 785 """ 786 self._source_credentials = source_credentials 787 self._mfa_prompter = mfa_prompter 788 if self._mfa_prompter is None: 789 self._mfa_prompter = getpass.getpass 790 791 super(AssumeRoleCredentialFetcher, self).__init__( 792 client_creator, role_arn, extra_args=extra_args, 793 cache=cache, expiry_window_seconds=expiry_window_seconds 794 ) 795 796 def _get_credentials(self): 797 """Get credentials by calling assume role.""" 798 kwargs = self._assume_role_kwargs() 799 client = self._create_client() 800 return client.assume_role(**kwargs) 801 802 def _assume_role_kwargs(self): 803 """Get the arguments for assume role based on current configuration.""" 804 assume_role_kwargs = deepcopy(self._assume_kwargs) 805 806 mfa_serial = assume_role_kwargs.get('SerialNumber') 807 808 if mfa_serial is not None: 809 prompt = 'Enter MFA code for %s: ' % mfa_serial 810 token_code = self._mfa_prompter(prompt) 811 assume_role_kwargs['TokenCode'] = token_code 812 813 duration_seconds = assume_role_kwargs.get('DurationSeconds') 814 815 if duration_seconds is not None: 816 assume_role_kwargs['DurationSeconds'] = duration_seconds 817 818 return assume_role_kwargs 819 820 def _create_client(self): 821 """Create an STS client using the source credentials.""" 822 frozen_credentials = self._source_credentials.get_frozen_credentials() 823 return self._client_creator( 824 'sts', 825 aws_access_key_id=frozen_credentials.access_key, 826 aws_secret_access_key=frozen_credentials.secret_key, 827 aws_session_token=frozen_credentials.token, 828 ) 829 830 831class AssumeRoleWithWebIdentityCredentialFetcher( 832 BaseAssumeRoleCredentialFetcher 833): 834 def __init__(self, client_creator, web_identity_token_loader, role_arn, 835 extra_args=None, cache=None, expiry_window_seconds=None): 836 """ 837 :type client_creator: callable 838 :param client_creator: A callable that creates a client taking 839 arguments like ``Session.create_client``. 840 841 :type web_identity_token_loader: callable 842 :param web_identity_token_loader: A callable that takes no arguments 843 and returns a web identity token str. 844 845 :type role_arn: str 846 :param role_arn: The ARN of the role to be assumed. 847 848 :type extra_args: dict 849 :param extra_args: Any additional arguments to add to the assume 850 role request using the format of the botocore operation. 851 Possible keys include, but may not be limited to, 852 DurationSeconds, Policy, SerialNumber, ExternalId and 853 RoleSessionName. 854 855 :type cache: dict 856 :param cache: An object that supports ``__getitem__``, 857 ``__setitem__``, and ``__contains__``. An example of this is 858 the ``JSONFileCache`` class in aws-cli. 859 860 :type expiry_window_seconds: int 861 :param expiry_window_seconds: The amount of time, in seconds, 862 """ 863 self._web_identity_token_loader = web_identity_token_loader 864 865 super(AssumeRoleWithWebIdentityCredentialFetcher, self).__init__( 866 client_creator, role_arn, extra_args=extra_args, 867 cache=cache, expiry_window_seconds=expiry_window_seconds 868 ) 869 870 def _get_credentials(self): 871 """Get credentials by calling assume role.""" 872 kwargs = self._assume_role_kwargs() 873 # Assume role with web identity does not require credentials other than 874 # the token, explicitly configure the client to not sign requests. 875 config = Config(signature_version=UNSIGNED) 876 client = self._client_creator('sts', config=config) 877 return client.assume_role_with_web_identity(**kwargs) 878 879 def _assume_role_kwargs(self): 880 """Get the arguments for assume role based on current configuration.""" 881 assume_role_kwargs = deepcopy(self._assume_kwargs) 882 identity_token = self._web_identity_token_loader() 883 assume_role_kwargs['WebIdentityToken'] = identity_token 884 885 return assume_role_kwargs 886 887 888class CredentialProvider(object): 889 # A short name to identify the provider within botocore. 890 METHOD = None 891 892 # A name to identify the provider for use in cross-sdk features like 893 # assume role's `credential_source` configuration option. These names 894 # are to be treated in a case-insensitive way. NOTE: any providers not 895 # implemented in botocore MUST prefix their canonical names with 896 # 'custom' or we DO NOT guarantee that it will work with any features 897 # that this provides. 898 CANONICAL_NAME = None 899 900 def __init__(self, session=None): 901 self.session = session 902 903 def load(self): 904 """ 905 Loads the credentials from their source & sets them on the object. 906 907 Subclasses should implement this method (by reading from disk, the 908 environment, the network or wherever), returning ``True`` if they were 909 found & loaded. 910 911 If not found, this method should return ``False``, indictating that the 912 ``CredentialResolver`` should fall back to the next available method. 913 914 The default implementation does nothing, assuming the user has set the 915 ``access_key/secret_key/token`` themselves. 916 917 :returns: Whether credentials were found & set 918 :rtype: Credentials 919 """ 920 return True 921 922 def _extract_creds_from_mapping(self, mapping, *key_names): 923 found = [] 924 for key_name in key_names: 925 try: 926 found.append(mapping[key_name]) 927 except KeyError: 928 raise PartialCredentialsError(provider=self.METHOD, 929 cred_var=key_name) 930 return found 931 932 933class ProcessProvider(CredentialProvider): 934 935 METHOD = 'custom-process' 936 937 def __init__(self, profile_name, load_config, popen=subprocess.Popen): 938 self._profile_name = profile_name 939 self._load_config = load_config 940 self._loaded_config = None 941 self._popen = popen 942 943 def load(self): 944 credential_process = self._credential_process 945 if credential_process is None: 946 return 947 948 creds_dict = self._retrieve_credentials_using(credential_process) 949 if creds_dict.get('expiry_time') is not None: 950 return RefreshableCredentials.create_from_metadata( 951 creds_dict, 952 lambda: self._retrieve_credentials_using(credential_process), 953 self.METHOD 954 ) 955 956 return Credentials( 957 access_key=creds_dict['access_key'], 958 secret_key=creds_dict['secret_key'], 959 token=creds_dict.get('token'), 960 method=self.METHOD 961 ) 962 963 def _retrieve_credentials_using(self, credential_process): 964 # We're not using shell=True, so we need to pass the 965 # command and all arguments as a list. 966 process_list = compat_shell_split(credential_process) 967 p = self._popen(process_list, 968 stdout=subprocess.PIPE, 969 stderr=subprocess.PIPE) 970 stdout, stderr = p.communicate() 971 if p.returncode != 0: 972 raise CredentialRetrievalError( 973 provider=self.METHOD, error_msg=stderr.decode('utf-8')) 974 parsed = botocore.compat.json.loads(stdout.decode('utf-8')) 975 version = parsed.get('Version', '<Version key not provided>') 976 if version != 1: 977 raise CredentialRetrievalError( 978 provider=self.METHOD, 979 error_msg=("Unsupported version '%s' for credential process " 980 "provider, supported versions: 1" % version)) 981 try: 982 return { 983 'access_key': parsed['AccessKeyId'], 984 'secret_key': parsed['SecretAccessKey'], 985 'token': parsed.get('SessionToken'), 986 'expiry_time': parsed.get('Expiration'), 987 } 988 except KeyError as e: 989 raise CredentialRetrievalError( 990 provider=self.METHOD, 991 error_msg="Missing required key in response: %s" % e 992 ) 993 994 @property 995 def _credential_process(self): 996 if self._loaded_config is None: 997 self._loaded_config = self._load_config() 998 profile_config = self._loaded_config.get( 999 'profiles', {}).get(self._profile_name, {}) 1000 return profile_config.get('credential_process') 1001 1002 1003class InstanceMetadataProvider(CredentialProvider): 1004 METHOD = 'iam-role' 1005 CANONICAL_NAME = 'Ec2InstanceMetadata' 1006 1007 def __init__(self, iam_role_fetcher): 1008 self._role_fetcher = iam_role_fetcher 1009 1010 def load(self): 1011 fetcher = self._role_fetcher 1012 # We do the first request, to see if we get useful data back. 1013 # If not, we'll pass & move on to whatever's next in the credential 1014 # chain. 1015 metadata = fetcher.retrieve_iam_role_credentials() 1016 if not metadata: 1017 return None 1018 logger.debug('Found credentials from IAM Role: %s', 1019 metadata['role_name']) 1020 # We manually set the data here, since we already made the request & 1021 # have it. When the expiry is hit, the credentials will auto-refresh 1022 # themselves. 1023 creds = RefreshableCredentials.create_from_metadata( 1024 metadata, 1025 method=self.METHOD, 1026 refresh_using=fetcher.retrieve_iam_role_credentials, 1027 ) 1028 return creds 1029 1030 1031class EnvProvider(CredentialProvider): 1032 METHOD = 'env' 1033 CANONICAL_NAME = 'Environment' 1034 ACCESS_KEY = 'AWS_ACCESS_KEY_ID' 1035 SECRET_KEY = 'AWS_SECRET_ACCESS_KEY' 1036 # The token can come from either of these env var. 1037 # AWS_SESSION_TOKEN is what other AWS SDKs have standardized on. 1038 TOKENS = ['AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN'] 1039 EXPIRY_TIME = 'AWS_CREDENTIAL_EXPIRATION' 1040 1041 def __init__(self, environ=None, mapping=None): 1042 """ 1043 1044 :param environ: The environment variables (defaults to 1045 ``os.environ`` if no value is provided). 1046 :param mapping: An optional mapping of variable names to 1047 environment variable names. Use this if you want to 1048 change the mapping of access_key->AWS_ACCESS_KEY_ID, etc. 1049 The dict can have up to 3 keys: ``access_key``, ``secret_key``, 1050 ``session_token``. 1051 """ 1052 if environ is None: 1053 environ = os.environ 1054 self.environ = environ 1055 self._mapping = self._build_mapping(mapping) 1056 1057 def _build_mapping(self, mapping): 1058 # Mapping of variable name to env var name. 1059 var_mapping = {} 1060 if mapping is None: 1061 # Use the class var default. 1062 var_mapping['access_key'] = self.ACCESS_KEY 1063 var_mapping['secret_key'] = self.SECRET_KEY 1064 var_mapping['token'] = self.TOKENS 1065 var_mapping['expiry_time'] = self.EXPIRY_TIME 1066 else: 1067 var_mapping['access_key'] = mapping.get( 1068 'access_key', self.ACCESS_KEY) 1069 var_mapping['secret_key'] = mapping.get( 1070 'secret_key', self.SECRET_KEY) 1071 var_mapping['token'] = mapping.get( 1072 'token', self.TOKENS) 1073 if not isinstance(var_mapping['token'], list): 1074 var_mapping['token'] = [var_mapping['token']] 1075 var_mapping['expiry_time'] = mapping.get( 1076 'expiry_time', self.EXPIRY_TIME) 1077 return var_mapping 1078 1079 def load(self): 1080 """ 1081 Search for credentials in explicit environment variables. 1082 """ 1083 1084 access_key = self.environ.get(self._mapping['access_key'], '') 1085 1086 if access_key: 1087 logger.info('Found credentials in environment variables.') 1088 fetcher = self._create_credentials_fetcher() 1089 credentials = fetcher(require_expiry=False) 1090 1091 expiry_time = credentials['expiry_time'] 1092 if expiry_time is not None: 1093 expiry_time = parse(expiry_time) 1094 return RefreshableCredentials( 1095 credentials['access_key'], credentials['secret_key'], 1096 credentials['token'], expiry_time, 1097 refresh_using=fetcher, method=self.METHOD 1098 ) 1099 1100 return Credentials( 1101 credentials['access_key'], credentials['secret_key'], 1102 credentials['token'], method=self.METHOD 1103 ) 1104 else: 1105 return None 1106 1107 def _create_credentials_fetcher(self): 1108 mapping = self._mapping 1109 method = self.METHOD 1110 environ = self.environ 1111 1112 def fetch_credentials(require_expiry=True): 1113 credentials = {} 1114 1115 access_key = environ.get(mapping['access_key'], '') 1116 if not access_key: 1117 raise PartialCredentialsError( 1118 provider=method, cred_var=mapping['access_key']) 1119 credentials['access_key'] = access_key 1120 1121 secret_key = environ.get(mapping['secret_key'], '') 1122 if not secret_key: 1123 raise PartialCredentialsError( 1124 provider=method, cred_var=mapping['secret_key']) 1125 credentials['secret_key'] = secret_key 1126 1127 credentials['token'] = None 1128 for token_env_var in mapping['token']: 1129 token = environ.get(token_env_var, '') 1130 if token: 1131 credentials['token'] = token 1132 break 1133 1134 credentials['expiry_time'] = None 1135 expiry_time = environ.get(mapping['expiry_time'], '') 1136 if expiry_time: 1137 credentials['expiry_time'] = expiry_time 1138 if require_expiry and not expiry_time: 1139 raise PartialCredentialsError( 1140 provider=method, cred_var=mapping['expiry_time']) 1141 1142 return credentials 1143 1144 return fetch_credentials 1145 1146 1147class OriginalEC2Provider(CredentialProvider): 1148 METHOD = 'ec2-credentials-file' 1149 CANONICAL_NAME = 'Ec2Config' 1150 1151 CRED_FILE_ENV = 'AWS_CREDENTIAL_FILE' 1152 ACCESS_KEY = 'AWSAccessKeyId' 1153 SECRET_KEY = 'AWSSecretKey' 1154 1155 def __init__(self, environ=None, parser=None): 1156 if environ is None: 1157 environ = os.environ 1158 if parser is None: 1159 parser = parse_key_val_file 1160 self._environ = environ 1161 self._parser = parser 1162 1163 def load(self): 1164 """ 1165 Search for a credential file used by original EC2 CLI tools. 1166 """ 1167 if 'AWS_CREDENTIAL_FILE' in self._environ: 1168 full_path = os.path.expanduser( 1169 self._environ['AWS_CREDENTIAL_FILE']) 1170 creds = self._parser(full_path) 1171 if self.ACCESS_KEY in creds: 1172 logger.info('Found credentials in AWS_CREDENTIAL_FILE.') 1173 access_key = creds[self.ACCESS_KEY] 1174 secret_key = creds[self.SECRET_KEY] 1175 # EC2 creds file doesn't support session tokens. 1176 return Credentials(access_key, secret_key, method=self.METHOD) 1177 else: 1178 return None 1179 1180 1181class SharedCredentialProvider(CredentialProvider): 1182 METHOD = 'shared-credentials-file' 1183 CANONICAL_NAME = 'SharedCredentials' 1184 1185 ACCESS_KEY = 'aws_access_key_id' 1186 SECRET_KEY = 'aws_secret_access_key' 1187 # Same deal as the EnvProvider above. Botocore originally supported 1188 # aws_security_token, but the SDKs are standardizing on aws_session_token 1189 # so we support both. 1190 TOKENS = ['aws_security_token', 'aws_session_token'] 1191 1192 def __init__(self, creds_filename, profile_name=None, ini_parser=None): 1193 self._creds_filename = creds_filename 1194 if profile_name is None: 1195 profile_name = 'default' 1196 self._profile_name = profile_name 1197 if ini_parser is None: 1198 ini_parser = botocore.configloader.raw_config_parse 1199 self._ini_parser = ini_parser 1200 1201 def load(self): 1202 try: 1203 available_creds = self._ini_parser(self._creds_filename) 1204 except ConfigNotFound: 1205 return None 1206 if self._profile_name in available_creds: 1207 config = available_creds[self._profile_name] 1208 if self.ACCESS_KEY in config: 1209 logger.info("Found credentials in shared credentials file: %s", 1210 self._creds_filename) 1211 access_key, secret_key = self._extract_creds_from_mapping( 1212 config, self.ACCESS_KEY, self.SECRET_KEY) 1213 token = self._get_session_token(config) 1214 return Credentials(access_key, secret_key, token, 1215 method=self.METHOD) 1216 1217 def _get_session_token(self, config): 1218 for token_envvar in self.TOKENS: 1219 if token_envvar in config: 1220 return config[token_envvar] 1221 1222 1223class ConfigProvider(CredentialProvider): 1224 """INI based config provider with profile sections.""" 1225 METHOD = 'config-file' 1226 CANONICAL_NAME = 'SharedConfig' 1227 1228 ACCESS_KEY = 'aws_access_key_id' 1229 SECRET_KEY = 'aws_secret_access_key' 1230 # Same deal as the EnvProvider above. Botocore originally supported 1231 # aws_security_token, but the SDKs are standardizing on aws_session_token 1232 # so we support both. 1233 TOKENS = ['aws_security_token', 'aws_session_token'] 1234 1235 def __init__(self, config_filename, profile_name, config_parser=None): 1236 """ 1237 1238 :param config_filename: The session configuration scoped to the current 1239 profile. This is available via ``session.config``. 1240 :param profile_name: The name of the current profile. 1241 :param config_parser: A config parser callable. 1242 1243 """ 1244 self._config_filename = config_filename 1245 self._profile_name = profile_name 1246 if config_parser is None: 1247 config_parser = botocore.configloader.load_config 1248 self._config_parser = config_parser 1249 1250 def load(self): 1251 """ 1252 If there is are credentials in the configuration associated with 1253 the session, use those. 1254 """ 1255 try: 1256 full_config = self._config_parser(self._config_filename) 1257 except ConfigNotFound: 1258 return None 1259 if self._profile_name in full_config['profiles']: 1260 profile_config = full_config['profiles'][self._profile_name] 1261 if self.ACCESS_KEY in profile_config: 1262 logger.info("Credentials found in config file: %s", 1263 self._config_filename) 1264 access_key, secret_key = self._extract_creds_from_mapping( 1265 profile_config, self.ACCESS_KEY, self.SECRET_KEY) 1266 token = self._get_session_token(profile_config) 1267 return Credentials(access_key, secret_key, token, 1268 method=self.METHOD) 1269 else: 1270 return None 1271 1272 def _get_session_token(self, profile_config): 1273 for token_name in self.TOKENS: 1274 if token_name in profile_config: 1275 return profile_config[token_name] 1276 1277 1278class BotoProvider(CredentialProvider): 1279 METHOD = 'boto-config' 1280 CANONICAL_NAME = 'Boto2Config' 1281 1282 BOTO_CONFIG_ENV = 'BOTO_CONFIG' 1283 DEFAULT_CONFIG_FILENAMES = ['/etc/boto.cfg', '~/.boto'] 1284 ACCESS_KEY = 'aws_access_key_id' 1285 SECRET_KEY = 'aws_secret_access_key' 1286 1287 def __init__(self, environ=None, ini_parser=None): 1288 if environ is None: 1289 environ = os.environ 1290 if ini_parser is None: 1291 ini_parser = botocore.configloader.raw_config_parse 1292 self._environ = environ 1293 self._ini_parser = ini_parser 1294 1295 def load(self): 1296 """ 1297 Look for credentials in boto config file. 1298 """ 1299 if self.BOTO_CONFIG_ENV in self._environ: 1300 potential_locations = [self._environ[self.BOTO_CONFIG_ENV]] 1301 else: 1302 potential_locations = self.DEFAULT_CONFIG_FILENAMES 1303 for filename in potential_locations: 1304 try: 1305 config = self._ini_parser(filename) 1306 except ConfigNotFound: 1307 # Move on to the next potential config file name. 1308 continue 1309 if 'Credentials' in config: 1310 credentials = config['Credentials'] 1311 if self.ACCESS_KEY in credentials: 1312 logger.info("Found credentials in boto config file: %s", 1313 filename) 1314 access_key, secret_key = self._extract_creds_from_mapping( 1315 credentials, self.ACCESS_KEY, self.SECRET_KEY) 1316 return Credentials(access_key, secret_key, 1317 method=self.METHOD) 1318 1319 1320class AssumeRoleProvider(CredentialProvider): 1321 METHOD = 'assume-role' 1322 # The AssumeRole provider is logically part of the SharedConfig and 1323 # SharedCredentials providers. Since the purpose of the canonical name 1324 # is to provide cross-sdk compatibility, calling code will need to be 1325 # aware that either of those providers should be tied to the AssumeRole 1326 # provider as much as possible. 1327 CANONICAL_NAME = None 1328 ROLE_CONFIG_VAR = 'role_arn' 1329 WEB_IDENTITY_TOKE_FILE_VAR = 'web_identity_token_file' 1330 # Credentials are considered expired (and will be refreshed) once the total 1331 # remaining time left until the credentials expires is less than the 1332 # EXPIRY_WINDOW. 1333 EXPIRY_WINDOW_SECONDS = 60 * 15 1334 1335 def __init__(self, load_config, client_creator, cache, profile_name, 1336 prompter=getpass.getpass, credential_sourcer=None, 1337 profile_provider_builder=None): 1338 """ 1339 :type load_config: callable 1340 :param load_config: A function that accepts no arguments, and 1341 when called, will return the full configuration dictionary 1342 for the session (``session.full_config``). 1343 1344 :type client_creator: callable 1345 :param client_creator: A factory function that will create 1346 a client when called. Has the same interface as 1347 ``botocore.session.Session.create_client``. 1348 1349 :type cache: dict 1350 :param cache: An object that supports ``__getitem__``, 1351 ``__setitem__``, and ``__contains__``. An example 1352 of this is the ``JSONFileCache`` class in the CLI. 1353 1354 :type profile_name: str 1355 :param profile_name: The name of the profile. 1356 1357 :type prompter: callable 1358 :param prompter: A callable that returns input provided 1359 by the user (i.e raw_input, getpass.getpass, etc.). 1360 1361 :type credential_sourcer: CanonicalNameCredentialSourcer 1362 :param credential_sourcer: A credential provider that takes a 1363 configuration, which is used to provide the source credentials 1364 for the STS call. 1365 """ 1366 #: The cache used to first check for assumed credentials. 1367 #: This is checked before making the AssumeRole API 1368 #: calls and can be useful if you have short lived 1369 #: scripts and you'd like to avoid calling AssumeRole 1370 #: until the credentials are expired. 1371 self.cache = cache 1372 self._load_config = load_config 1373 # client_creator is a callable that creates function. 1374 # It's basically session.create_client 1375 self._client_creator = client_creator 1376 self._profile_name = profile_name 1377 self._prompter = prompter 1378 # The _loaded_config attribute will be populated from the 1379 # load_config() function once the configuration is actually 1380 # loaded. The reason we go through all this instead of just 1381 # requiring that the loaded_config be passed to us is to that 1382 # we can defer configuration loaded until we actually try 1383 # to load credentials (as opposed to when the object is 1384 # instantiated). 1385 self._loaded_config = {} 1386 self._credential_sourcer = credential_sourcer 1387 self._profile_provider_builder = profile_provider_builder 1388 self._visited_profiles = [self._profile_name] 1389 1390 def load(self): 1391 self._loaded_config = self._load_config() 1392 profiles = self._loaded_config.get('profiles', {}) 1393 profile = profiles.get(self._profile_name, {}) 1394 if self._has_assume_role_config_vars(profile): 1395 return self._load_creds_via_assume_role(self._profile_name) 1396 1397 def _has_assume_role_config_vars(self, profile): 1398 return ( 1399 self.ROLE_CONFIG_VAR in profile and 1400 # We need to ensure this provider doesn't look at a profile when 1401 # the profile has configuration for web identity. Simply relying on 1402 # the order in the credential chain is insufficient as it doesn't 1403 # prevent the case when we're doing an assume role chain. 1404 self.WEB_IDENTITY_TOKE_FILE_VAR not in profile 1405 ) 1406 1407 def _load_creds_via_assume_role(self, profile_name): 1408 role_config = self._get_role_config(profile_name) 1409 source_credentials = self._resolve_source_credentials( 1410 role_config, profile_name 1411 ) 1412 1413 extra_args = {} 1414 role_session_name = role_config.get('role_session_name') 1415 if role_session_name is not None: 1416 extra_args['RoleSessionName'] = role_session_name 1417 1418 external_id = role_config.get('external_id') 1419 if external_id is not None: 1420 extra_args['ExternalId'] = external_id 1421 1422 mfa_serial = role_config.get('mfa_serial') 1423 if mfa_serial is not None: 1424 extra_args['SerialNumber'] = mfa_serial 1425 1426 duration_seconds = role_config.get('duration_seconds') 1427 if duration_seconds is not None: 1428 extra_args['DurationSeconds'] = duration_seconds 1429 1430 fetcher = AssumeRoleCredentialFetcher( 1431 client_creator=self._client_creator, 1432 source_credentials=source_credentials, 1433 role_arn=role_config['role_arn'], 1434 extra_args=extra_args, 1435 mfa_prompter=self._prompter, 1436 cache=self.cache, 1437 ) 1438 refresher = fetcher.fetch_credentials 1439 if mfa_serial is not None: 1440 refresher = create_mfa_serial_refresher(refresher) 1441 1442 # The initial credentials are empty and the expiration time is set 1443 # to now so that we can delay the call to assume role until it is 1444 # strictly needed. 1445 return DeferredRefreshableCredentials( 1446 method=self.METHOD, 1447 refresh_using=refresher, 1448 time_fetcher=_local_now 1449 ) 1450 1451 def _get_role_config(self, profile_name): 1452 """Retrieves and validates the role configuration for the profile.""" 1453 profiles = self._loaded_config.get('profiles', {}) 1454 1455 profile = profiles[profile_name] 1456 source_profile = profile.get('source_profile') 1457 role_arn = profile['role_arn'] 1458 credential_source = profile.get('credential_source') 1459 mfa_serial = profile.get('mfa_serial') 1460 external_id = profile.get('external_id') 1461 role_session_name = profile.get('role_session_name') 1462 duration_seconds = profile.get('duration_seconds') 1463 1464 role_config = { 1465 'role_arn': role_arn, 1466 'external_id': external_id, 1467 'mfa_serial': mfa_serial, 1468 'role_session_name': role_session_name, 1469 'source_profile': source_profile, 1470 'credential_source': credential_source 1471 } 1472 1473 if duration_seconds is not None: 1474 try: 1475 role_config['duration_seconds'] = int(duration_seconds) 1476 except ValueError: 1477 pass 1478 1479 # Either the credential source or the source profile must be 1480 # specified, but not both. 1481 if credential_source is not None and source_profile is not None: 1482 raise InvalidConfigError( 1483 error_msg=( 1484 'The profile "%s" contains both source_profile and ' 1485 'credential_source.' % profile_name 1486 ) 1487 ) 1488 elif credential_source is None and source_profile is None: 1489 raise PartialCredentialsError( 1490 provider=self.METHOD, 1491 cred_var='source_profile or credential_source' 1492 ) 1493 elif credential_source is not None: 1494 self._validate_credential_source( 1495 profile_name, credential_source) 1496 else: 1497 self._validate_source_profile(profile_name, source_profile) 1498 1499 return role_config 1500 1501 def _validate_credential_source(self, parent_profile, credential_source): 1502 if self._credential_sourcer is None: 1503 raise InvalidConfigError(error_msg=( 1504 'The credential_source "%s" is specified in profile "%s", ' 1505 'but no source provider was configured.' % ( 1506 credential_source, parent_profile) 1507 )) 1508 if not self._credential_sourcer.is_supported(credential_source): 1509 raise InvalidConfigError(error_msg=( 1510 'The credential source "%s" referenced in profile "%s" is not ' 1511 'valid.' % (credential_source, parent_profile) 1512 )) 1513 1514 def _source_profile_has_credentials(self, profile): 1515 return any([ 1516 self._has_static_credentials(profile), 1517 self._has_assume_role_config_vars(profile), 1518 ]) 1519 1520 def _validate_source_profile(self, parent_profile_name, 1521 source_profile_name): 1522 profiles = self._loaded_config.get('profiles', {}) 1523 if source_profile_name not in profiles: 1524 raise InvalidConfigError( 1525 error_msg=( 1526 'The source_profile "%s" referenced in ' 1527 'the profile "%s" does not exist.' % ( 1528 source_profile_name, parent_profile_name) 1529 ) 1530 ) 1531 1532 source_profile = profiles[source_profile_name] 1533 1534 # Make sure we aren't going into an infinite loop. If we haven't 1535 # visited the profile yet, we're good. 1536 if source_profile_name not in self._visited_profiles: 1537 return 1538 1539 # If we have visited the profile and the profile isn't simply 1540 # referencing itself, that's an infinite loop. 1541 if source_profile_name != parent_profile_name: 1542 raise InfiniteLoopConfigError( 1543 source_profile=source_profile_name, 1544 visited_profiles=self._visited_profiles 1545 ) 1546 1547 # A profile is allowed to reference itself so that it can source 1548 # static credentials and have configuration all in the same 1549 # profile. This will only ever work for the top level assume 1550 # role because the static credentials will otherwise take 1551 # precedence. 1552 if not self._has_static_credentials(source_profile): 1553 raise InfiniteLoopConfigError( 1554 source_profile=source_profile_name, 1555 visited_profiles=self._visited_profiles 1556 ) 1557 1558 def _has_static_credentials(self, profile): 1559 static_keys = ['aws_secret_access_key', 'aws_access_key_id'] 1560 return any(static_key in profile for static_key in static_keys) 1561 1562 def _resolve_source_credentials(self, role_config, profile_name): 1563 credential_source = role_config.get('credential_source') 1564 if credential_source is not None: 1565 return self._resolve_credentials_from_source( 1566 credential_source, profile_name 1567 ) 1568 1569 source_profile = role_config['source_profile'] 1570 self._visited_profiles.append(source_profile) 1571 return self._resolve_credentials_from_profile(source_profile) 1572 1573 def _resolve_credentials_from_profile(self, profile_name): 1574 profiles = self._loaded_config.get('profiles', {}) 1575 profile = profiles[profile_name] 1576 1577 if self._has_static_credentials(profile) and \ 1578 not self._profile_provider_builder: 1579 # This is only here for backwards compatibility. If this provider 1580 # isn't given a profile provider builder we still want to be able 1581 # handle the basic static credential case as we would before the 1582 # provile provider builder parameter was added. 1583 return self._resolve_static_credentials_from_profile(profile) 1584 elif self._has_static_credentials(profile) or \ 1585 not self._has_assume_role_config_vars(profile): 1586 profile_providers = self._profile_provider_builder.providers( 1587 profile_name=profile_name, 1588 disable_env_vars=True, 1589 ) 1590 profile_chain = CredentialResolver(profile_providers) 1591 credentials = profile_chain.load_credentials() 1592 if credentials is None: 1593 error_message = ( 1594 'The source profile "%s" must have credentials.' 1595 ) 1596 raise InvalidConfigError( 1597 error_msg=error_message % profile_name, 1598 ) 1599 return credentials 1600 1601 return self._load_creds_via_assume_role(profile_name) 1602 1603 def _resolve_static_credentials_from_profile(self, profile): 1604 try: 1605 return Credentials( 1606 access_key=profile['aws_access_key_id'], 1607 secret_key=profile['aws_secret_access_key'], 1608 token=profile.get('aws_session_token') 1609 ) 1610 except KeyError as e: 1611 raise PartialCredentialsError( 1612 provider=self.METHOD, cred_var=str(e)) 1613 1614 def _resolve_credentials_from_source(self, credential_source, 1615 profile_name): 1616 credentials = self._credential_sourcer.source_credentials( 1617 credential_source) 1618 if credentials is None: 1619 raise CredentialRetrievalError( 1620 provider=credential_source, 1621 error_msg=( 1622 'No credentials found in credential_source referenced ' 1623 'in profile %s' % profile_name 1624 ) 1625 ) 1626 return credentials 1627 1628 1629class AssumeRoleWithWebIdentityProvider(CredentialProvider): 1630 METHOD = 'assume-role-with-web-identity' 1631 CANONICAL_NAME = None 1632 _CONFIG_TO_ENV_VAR = { 1633 'web_identity_token_file': 'AWS_WEB_IDENTITY_TOKEN_FILE', 1634 'role_session_name': 'AWS_ROLE_SESSION_NAME', 1635 'role_arn': 'AWS_ROLE_ARN', 1636 } 1637 1638 def __init__( 1639 self, 1640 load_config, 1641 client_creator, 1642 profile_name, 1643 cache=None, 1644 disable_env_vars=False, 1645 token_loader_cls=None, 1646 ): 1647 self.cache = cache 1648 self._load_config = load_config 1649 self._client_creator = client_creator 1650 self._profile_name = profile_name 1651 self._profile_config = None 1652 self._disable_env_vars = disable_env_vars 1653 if token_loader_cls is None: 1654 token_loader_cls = FileWebIdentityTokenLoader 1655 self._token_loader_cls = token_loader_cls 1656 1657 def load(self): 1658 return self._assume_role_with_web_identity() 1659 1660 def _get_profile_config(self, key): 1661 if self._profile_config is None: 1662 loaded_config = self._load_config() 1663 profiles = loaded_config.get('profiles', {}) 1664 self._profile_config = profiles.get(self._profile_name, {}) 1665 return self._profile_config.get(key) 1666 1667 def _get_env_config(self, key): 1668 if self._disable_env_vars: 1669 return None 1670 env_key = self._CONFIG_TO_ENV_VAR.get(key) 1671 if env_key and env_key in os.environ: 1672 return os.environ[env_key] 1673 return None 1674 1675 def _get_config(self, key): 1676 env_value = self._get_env_config(key) 1677 if env_value is not None: 1678 return env_value 1679 return self._get_profile_config(key) 1680 1681 def _assume_role_with_web_identity(self): 1682 token_path = self._get_config('web_identity_token_file') 1683 if not token_path: 1684 return None 1685 token_loader = self._token_loader_cls(token_path) 1686 1687 role_arn = self._get_config('role_arn') 1688 if not role_arn: 1689 error_msg = ( 1690 'The provided profile or the current environment is ' 1691 'configured to assume role with web identity but has no ' 1692 'role ARN configured. Ensure that the profile has the role_arn' 1693 'configuration set or the AWS_ROLE_ARN env var is set.' 1694 ) 1695 raise InvalidConfigError(error_msg=error_msg) 1696 1697 extra_args = {} 1698 role_session_name = self._get_config('role_session_name') 1699 if role_session_name is not None: 1700 extra_args['RoleSessionName'] = role_session_name 1701 1702 fetcher = AssumeRoleWithWebIdentityCredentialFetcher( 1703 client_creator=self._client_creator, 1704 web_identity_token_loader=token_loader, 1705 role_arn=role_arn, 1706 extra_args=extra_args, 1707 cache=self.cache, 1708 ) 1709 # The initial credentials are empty and the expiration time is set 1710 # to now so that we can delay the call to assume role until it is 1711 # strictly needed. 1712 return DeferredRefreshableCredentials( 1713 method=self.METHOD, 1714 refresh_using=fetcher.fetch_credentials, 1715 ) 1716 1717 1718class CanonicalNameCredentialSourcer(object): 1719 def __init__(self, providers): 1720 self._providers = providers 1721 1722 def is_supported(self, source_name): 1723 """Validates a given source name. 1724 1725 :type source_name: str 1726 :param source_name: The value of credential_source in the config 1727 file. This is the canonical name of the credential provider. 1728 1729 :rtype: bool 1730 :returns: True if the credential provider is supported, 1731 False otherwise. 1732 """ 1733 return source_name in [p.CANONICAL_NAME for p in self._providers] 1734 1735 def source_credentials(self, source_name): 1736 """Loads source credentials based on the provided configuration. 1737 1738 :type source_name: str 1739 :param source_name: The value of credential_source in the config 1740 file. This is the canonical name of the credential provider. 1741 1742 :rtype: Credentials 1743 """ 1744 source = self._get_provider(source_name) 1745 if isinstance(source, CredentialResolver): 1746 return source.load_credentials() 1747 return source.load() 1748 1749 def _get_provider(self, canonical_name): 1750 """Return a credential provider by its canonical name. 1751 1752 :type canonical_name: str 1753 :param canonical_name: The canonical name of the provider. 1754 1755 :raises UnknownCredentialError: Raised if no 1756 credential provider by the provided name 1757 is found. 1758 """ 1759 provider = self._get_provider_by_canonical_name(canonical_name) 1760 1761 # The AssumeRole provider should really be part of the SharedConfig 1762 # provider rather than being its own thing, but it is not. It is 1763 # effectively part of both the SharedConfig provider and the 1764 # SharedCredentials provider now due to the way it behaves. 1765 # Therefore if we want either of those providers we should return 1766 # the AssumeRole provider with it. 1767 if canonical_name.lower() in ['sharedconfig', 'sharedcredentials']: 1768 assume_role_provider = self._get_provider_by_method('assume-role') 1769 if assume_role_provider is not None: 1770 # The SharedConfig or SharedCredentials provider may not be 1771 # present if it was removed for some reason, but the 1772 # AssumeRole provider could still be present. In that case, 1773 # return the assume role provider by itself. 1774 if provider is None: 1775 return assume_role_provider 1776 1777 # If both are present, return them both as a 1778 # CredentialResolver so that calling code can treat them as 1779 # a single entity. 1780 return CredentialResolver([assume_role_provider, provider]) 1781 1782 if provider is None: 1783 raise UnknownCredentialError(name=canonical_name) 1784 1785 return provider 1786 1787 def _get_provider_by_canonical_name(self, canonical_name): 1788 """Return a credential provider by its canonical name. 1789 1790 This function is strict, it does not attempt to address 1791 compatibility issues. 1792 """ 1793 for provider in self._providers: 1794 name = provider.CANONICAL_NAME 1795 # Canonical names are case-insensitive 1796 if name and name.lower() == canonical_name.lower(): 1797 return provider 1798 1799 def _get_provider_by_method(self, method): 1800 """Return a credential provider by its METHOD name.""" 1801 for provider in self._providers: 1802 if provider.METHOD == method: 1803 return provider 1804 1805 1806class ContainerProvider(CredentialProvider): 1807 METHOD = 'container-role' 1808 CANONICAL_NAME = 'EcsContainer' 1809 ENV_VAR = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' 1810 ENV_VAR_FULL = 'AWS_CONTAINER_CREDENTIALS_FULL_URI' 1811 ENV_VAR_AUTH_TOKEN = 'AWS_CONTAINER_AUTHORIZATION_TOKEN' 1812 1813 def __init__(self, environ=None, fetcher=None): 1814 if environ is None: 1815 environ = os.environ 1816 if fetcher is None: 1817 fetcher = ContainerMetadataFetcher() 1818 self._environ = environ 1819 self._fetcher = fetcher 1820 1821 def load(self): 1822 # This cred provider is only triggered if the self.ENV_VAR is set, 1823 # which only happens if you opt into this feature. 1824 if self.ENV_VAR in self._environ or self.ENV_VAR_FULL in self._environ: 1825 return self._retrieve_or_fail() 1826 1827 def _retrieve_or_fail(self): 1828 if self._provided_relative_uri(): 1829 full_uri = self._fetcher.full_url(self._environ[self.ENV_VAR]) 1830 else: 1831 full_uri = self._environ[self.ENV_VAR_FULL] 1832 headers = self._build_headers() 1833 fetcher = self._create_fetcher(full_uri, headers) 1834 creds = fetcher() 1835 return RefreshableCredentials( 1836 access_key=creds['access_key'], 1837 secret_key=creds['secret_key'], 1838 token=creds['token'], 1839 method=self.METHOD, 1840 expiry_time=_parse_if_needed(creds['expiry_time']), 1841 refresh_using=fetcher, 1842 ) 1843 1844 def _build_headers(self): 1845 headers = {} 1846 auth_token = self._environ.get(self.ENV_VAR_AUTH_TOKEN) 1847 if auth_token is not None: 1848 return { 1849 'Authorization': auth_token 1850 } 1851 1852 def _create_fetcher(self, full_uri, headers): 1853 def fetch_creds(): 1854 try: 1855 response = self._fetcher.retrieve_full_uri( 1856 full_uri, headers=headers) 1857 except MetadataRetrievalError as e: 1858 logger.debug("Error retrieving container metadata: %s", e, 1859 exc_info=True) 1860 raise CredentialRetrievalError(provider=self.METHOD, 1861 error_msg=str(e)) 1862 return { 1863 'access_key': response['AccessKeyId'], 1864 'secret_key': response['SecretAccessKey'], 1865 'token': response['Token'], 1866 'expiry_time': response['Expiration'], 1867 } 1868 1869 return fetch_creds 1870 1871 def _provided_relative_uri(self): 1872 return self.ENV_VAR in self._environ 1873 1874 1875class CredentialResolver(object): 1876 def __init__(self, providers): 1877 """ 1878 1879 :param providers: A list of ``CredentialProvider`` instances. 1880 1881 """ 1882 self.providers = providers 1883 1884 def insert_before(self, name, credential_provider): 1885 """ 1886 Inserts a new instance of ``CredentialProvider`` into the chain that 1887 will be tried before an existing one. 1888 1889 :param name: The short name of the credentials you'd like to insert the 1890 new credentials before. (ex. ``env`` or ``config``). Existing names 1891 & ordering can be discovered via ``self.available_methods``. 1892 :type name: string 1893 1894 :param cred_instance: An instance of the new ``Credentials`` object 1895 you'd like to add to the chain. 1896 :type cred_instance: A subclass of ``Credentials`` 1897 """ 1898 try: 1899 offset = [p.METHOD for p in self.providers].index(name) 1900 except ValueError: 1901 raise UnknownCredentialError(name=name) 1902 self.providers.insert(offset, credential_provider) 1903 1904 def insert_after(self, name, credential_provider): 1905 """ 1906 Inserts a new type of ``Credentials`` instance into the chain that will 1907 be tried after an existing one. 1908 1909 :param name: The short name of the credentials you'd like to insert the 1910 new credentials after. (ex. ``env`` or ``config``). Existing names 1911 & ordering can be discovered via ``self.available_methods``. 1912 :type name: string 1913 1914 :param cred_instance: An instance of the new ``Credentials`` object 1915 you'd like to add to the chain. 1916 :type cred_instance: A subclass of ``Credentials`` 1917 """ 1918 offset = self._get_provider_offset(name) 1919 self.providers.insert(offset + 1, credential_provider) 1920 1921 def remove(self, name): 1922 """ 1923 Removes a given ``Credentials`` instance from the chain. 1924 1925 :param name: The short name of the credentials instance to remove. 1926 :type name: string 1927 """ 1928 available_methods = [p.METHOD for p in self.providers] 1929 if name not in available_methods: 1930 # It's not present. Fail silently. 1931 return 1932 1933 offset = available_methods.index(name) 1934 self.providers.pop(offset) 1935 1936 def get_provider(self, name): 1937 """Return a credential provider by name. 1938 1939 :type name: str 1940 :param name: The name of the provider. 1941 1942 :raises UnknownCredentialError: Raised if no 1943 credential provider by the provided name 1944 is found. 1945 """ 1946 return self.providers[self._get_provider_offset(name)] 1947 1948 def _get_provider_offset(self, name): 1949 try: 1950 return [p.METHOD for p in self.providers].index(name) 1951 except ValueError: 1952 raise UnknownCredentialError(name=name) 1953 1954 def load_credentials(self): 1955 """ 1956 Goes through the credentials chain, returning the first ``Credentials`` 1957 that could be loaded. 1958 """ 1959 # First provider to return a non-None response wins. 1960 for provider in self.providers: 1961 logger.debug("Looking for credentials via: %s", provider.METHOD) 1962 creds = provider.load() 1963 if creds is not None: 1964 return creds 1965 1966 # If we got here, no credentials could be found. 1967 # This feels like it should be an exception, but historically, ``None`` 1968 # is returned. 1969 # 1970 # +1 1971 # -js 1972 return None 1973 1974 1975class SSOCredentialFetcher(CachedCredentialFetcher): 1976 def __init__(self, start_url, sso_region, role_name, account_id, 1977 client_creator, token_loader=None, cache=None, 1978 expiry_window_seconds=None): 1979 self._client_creator = client_creator 1980 self._sso_region = sso_region 1981 self._role_name = role_name 1982 self._account_id = account_id 1983 self._start_url = start_url 1984 self._token_loader = token_loader 1985 1986 super(SSOCredentialFetcher, self).__init__( 1987 cache, expiry_window_seconds 1988 ) 1989 1990 def _create_cache_key(self): 1991 """Create a predictable cache key for the current configuration. 1992 1993 The cache key is intended to be compatible with file names. 1994 """ 1995 args = { 1996 'startUrl': self._start_url, 1997 'roleName': self._role_name, 1998 'accountId': self._account_id, 1999 } 2000 # NOTE: It would be good to hoist this cache key construction logic 2001 # into the CachedCredentialFetcher class as we should be consistent. 2002 # Unfortunately, the current assume role fetchers that sub class don't 2003 # pass separators resulting in non-minified JSON. In the long term, 2004 # all fetchers should use the below caching scheme. 2005 args = json.dumps(args, sort_keys=True, separators=(',', ':')) 2006 argument_hash = sha1(args.encode('utf-8')).hexdigest() 2007 return self._make_file_safe(argument_hash) 2008 2009 def _parse_timestamp(self, timestamp_ms): 2010 # fromtimestamp expects seconds so: milliseconds / 1000 = seconds 2011 timestamp_seconds = timestamp_ms / 1000.0 2012 timestamp = datetime.datetime.fromtimestamp(timestamp_seconds, tzutc()) 2013 return _serialize_if_needed(timestamp) 2014 2015 def _get_credentials(self): 2016 """Get credentials by calling SSO get role credentials.""" 2017 config = Config( 2018 signature_version=UNSIGNED, 2019 region_name=self._sso_region, 2020 ) 2021 client = self._client_creator('sso', config=config) 2022 2023 kwargs = { 2024 'roleName': self._role_name, 2025 'accountId': self._account_id, 2026 'accessToken': self._token_loader(self._start_url), 2027 } 2028 try: 2029 response = client.get_role_credentials(**kwargs) 2030 except client.exceptions.UnauthorizedException: 2031 raise UnauthorizedSSOTokenError() 2032 credentials = response['roleCredentials'] 2033 2034 credentials = { 2035 'ProviderType': 'sso', 2036 'Credentials': { 2037 'AccessKeyId': credentials['accessKeyId'], 2038 'SecretAccessKey': credentials['secretAccessKey'], 2039 'SessionToken': credentials['sessionToken'], 2040 'Expiration': self._parse_timestamp(credentials['expiration']), 2041 } 2042 } 2043 return credentials 2044 2045 2046class SSOProvider(CredentialProvider): 2047 METHOD = 'sso' 2048 2049 _SSO_TOKEN_CACHE_DIR = os.path.expanduser( 2050 os.path.join('~', '.aws', 'sso', 'cache') 2051 ) 2052 _SSO_CONFIG_VARS = [ 2053 'sso_start_url', 2054 'sso_region', 2055 'sso_role_name', 2056 'sso_account_id', 2057 ] 2058 2059 def __init__(self, load_config, client_creator, profile_name, 2060 cache=None, token_cache=None): 2061 if token_cache is None: 2062 token_cache = JSONFileCache(self._SSO_TOKEN_CACHE_DIR) 2063 self._token_cache = token_cache 2064 if cache is None: 2065 cache = {} 2066 self.cache = cache 2067 self._load_config = load_config 2068 self._client_creator = client_creator 2069 self._profile_name = profile_name 2070 2071 def _load_sso_config(self): 2072 loaded_config = self._load_config() 2073 profiles = loaded_config.get('profiles', {}) 2074 profile_name = self._profile_name 2075 profile_config = profiles.get(self._profile_name, {}) 2076 2077 if all(c not in profile_config for c in self._SSO_CONFIG_VARS): 2078 return None 2079 2080 config = {} 2081 missing_config_vars = [] 2082 for config_var in self._SSO_CONFIG_VARS: 2083 if config_var in profile_config: 2084 config[config_var] = profile_config[config_var] 2085 else: 2086 missing_config_vars.append(config_var) 2087 2088 if missing_config_vars: 2089 missing = ', '.join(missing_config_vars) 2090 raise InvalidConfigError( 2091 error_msg=( 2092 'The profile "%s" is configured to use SSO but is missing ' 2093 'required configuration: %s' % (profile_name, missing) 2094 ) 2095 ) 2096 2097 return config 2098 2099 def load(self): 2100 sso_config = self._load_sso_config() 2101 if not sso_config: 2102 return None 2103 2104 sso_fetcher = SSOCredentialFetcher( 2105 sso_config['sso_start_url'], 2106 sso_config['sso_region'], 2107 sso_config['sso_role_name'], 2108 sso_config['sso_account_id'], 2109 self._client_creator, 2110 token_loader=SSOTokenLoader(cache=self._token_cache), 2111 cache=self.cache, 2112 ) 2113 2114 return DeferredRefreshableCredentials( 2115 method=self.METHOD, 2116 refresh_using=sso_fetcher.fetch_credentials, 2117 ) 2118