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.
14 11
import time
15 11
import datetime
16 11
import logging
17 11
import os
18 11
import getpass
19 11
import threading
20 11
import json
21 11
import subprocess
22 11
from collections import namedtuple
23 11
from copy import deepcopy
24 11
from hashlib import sha1
25

26 11
from dateutil.parser import parse
27 11
from dateutil.tz import tzlocal, tzutc
28

29 11
import botocore.configloader
30 11
import botocore.compat
31 11
from botocore import UNSIGNED
32 11
from botocore.compat import total_seconds
33 11
from botocore.compat import compat_shell_split
34 11
from botocore.config import Config
35 11
from botocore.exceptions import UnknownCredentialError
36 11
from botocore.exceptions import PartialCredentialsError
37 11
from botocore.exceptions import ConfigNotFound
38 11
from botocore.exceptions import InvalidConfigError
39 11
from botocore.exceptions import InfiniteLoopConfigError
40 11
from botocore.exceptions import RefreshWithMFAUnsupportedError
41 11
from botocore.exceptions import MetadataRetrievalError
42 11
from botocore.exceptions import CredentialRetrievalError
43 11
from botocore.exceptions import UnauthorizedSSOTokenError
44 11
from botocore.utils import InstanceMetadataFetcher, parse_key_val_file
45 11
from botocore.utils import ContainerMetadataFetcher
46 11
from botocore.utils import FileWebIdentityTokenLoader
47 11
from botocore.utils import SSOTokenLoader
48

49

50 11
logger = logging.getLogger(__name__)
51 11
ReadOnlyCredentials = namedtuple('ReadOnlyCredentials',
52
                                 ['access_key', 'secret_key', 'token'])
53

54

55 11
def 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 11
    profile_name = session.get_config_variable('profile') or 'default'
64 11
    metadata_timeout = session.get_config_variable('metadata_service_timeout')
65 11
    num_attempts = session.get_config_variable('metadata_service_num_attempts')
66 11
    disable_env_vars = session.instance_variables().get('profile') is not None
67

68 11
    imds_config = {
69
        'ec2_metadata_service_endpoint': session.get_config_variable(
70
            'ec2_metadata_service_endpoint'),
71
        'imds_use_ipv6': session.get_config_variable('imds_use_ipv6')
72
    }
73

74 11
    if cache is None:
75 11
        cache = {}
76

77 11
    env_provider = EnvProvider()
78 11
    container_provider = ContainerProvider()
79 11
    instance_metadata_provider = InstanceMetadataProvider(
80
        iam_role_fetcher=InstanceMetadataFetcher(
81
            timeout=metadata_timeout,
82
            num_attempts=num_attempts,
83
            user_agent=session.user_agent(),
84
            config=imds_config)
85
    )
86

87 11
    profile_provider_builder = ProfileProviderBuilder(
88
        session, cache=cache, region_name=region_name)
89 11
    assume_role_provider = AssumeRoleProvider(
90
        load_config=lambda: session.full_config,
91
        client_creator=_get_client_creator(session, region_name),
92
        cache=cache,
93
        profile_name=profile_name,
94
        credential_sourcer=CanonicalNameCredentialSourcer([
95
            env_provider, container_provider, instance_metadata_provider
96
        ]),
97
        profile_provider_builder=profile_provider_builder,
98
    )
99

100 11
    pre_profile = [
101
        env_provider,
102
        assume_role_provider,
103
    ]
104 11
    profile_providers = profile_provider_builder.providers(
105
        profile_name=profile_name,
106
        disable_env_vars=disable_env_vars,
107
    )
108 11
    post_profile = [
109
        OriginalEC2Provider(),
110
        BotoProvider(),
111
        container_provider,
112
        instance_metadata_provider,
113
    ]
114 11
    providers = pre_profile + profile_providers + post_profile
115

116 11
    if disable_env_vars:
117
        # An explicitly provided profile will negate an EnvProvider.
118
        # We will defer to providers that understand the "profile"
119
        # concept to retrieve credentials.
120
        # The one edge case if is all three values are provided via
121
        # env vars:
122
        # export AWS_ACCESS_KEY_ID=foo
123
        # export AWS_SECRET_ACCESS_KEY=bar
124
        # export AWS_PROFILE=baz
125
        # Then, just like our client() calls, the explicit credentials
126
        # will take precedence.
127
        #
128
        # This precedence is enforced by leaving the EnvProvider in the chain.
129
        # This means that the only way a "profile" would win is if the
130
        # EnvProvider does not return credentials, which is what we want
131
        # in this scenario.
132 11
        providers.remove(env_provider)
133 11
        logger.debug('Skipping environment variable credential check'
134
                     ' because profile name was explicitly set.')
135

136 11
    resolver = CredentialResolver(providers=providers)
137 11
    return resolver
138

139

140 11
class ProfileProviderBuilder(object):
141
    """This class handles the creation of profile based providers.
142

143
    NOTE: This class is only intended for internal use.
144

145
    This class handles the creation and ordering of the various credential
146
    providers that primarly source their configuration from the shared config.
147
    This is needed to enable sharing between the default credential chain and
148
    the source profile chain created by the assume role provider.
149
    """
150 11
    def __init__(self, session, cache=None, region_name=None,
151
                 sso_token_cache=None):
152 11
        self._session = session
153 11
        self._cache = cache
154 11
        self._region_name = region_name
155 11
        self._sso_token_cache = sso_token_cache
156

157 11
    def providers(self, profile_name, disable_env_vars=False):
158 11
        return [
159
            self._create_web_identity_provider(
160
                profile_name, disable_env_vars,
161
            ),
162
            self._create_sso_provider(profile_name),
163
            self._create_shared_credential_provider(profile_name),
164
            self._create_process_provider(profile_name),
165
            self._create_config_provider(profile_name),
166
        ]
167

168 11
    def _create_process_provider(self, profile_name):
169 11
        return ProcessProvider(
170
            profile_name=profile_name,
171
            load_config=lambda: self._session.full_config,
172
        )
173

174 11
    def _create_shared_credential_provider(self, profile_name):
175 11
        credential_file = self._session.get_config_variable('credentials_file')
176 11
        return SharedCredentialProvider(
177
            profile_name=profile_name,
178
            creds_filename=credential_file,
179
        )
180

181 11
    def _create_config_provider(self, profile_name):
182 11
        config_file = self._session.get_config_variable('config_file')
183 11
        return ConfigProvider(
184
            profile_name=profile_name,
185
            config_filename=config_file,
186
        )
187

188 11
    def _create_web_identity_provider(self, profile_name, disable_env_vars):
189 11
        return AssumeRoleWithWebIdentityProvider(
190
            load_config=lambda: self._session.full_config,
191
            client_creator=_get_client_creator(
192
                self._session, self._region_name),
193
            cache=self._cache,
194
            profile_name=profile_name,
195
            disable_env_vars=disable_env_vars,
196
        )
197

198 11
    def _create_sso_provider(self, profile_name):
199 11
        return SSOProvider(
200
            load_config=lambda: self._session.full_config,
201
            client_creator=self._session.create_client,
202
            profile_name=profile_name,
203
            cache=self._cache,
204
            token_cache=self._sso_token_cache,
205
        )
206

207

208 11
def get_credentials(session):
209 0
    resolver = create_credential_resolver(session)
210 0
    return resolver.load_credentials()
211

212

213 11
def _local_now():
214 11
    return datetime.datetime.now(tzlocal())
215

216

217 11
def _parse_if_needed(value):
218 11
    if isinstance(value, datetime.datetime):
219 0
        return value
220 11
    return parse(value)
221

222

223 11
def _serialize_if_needed(value, iso=False):
224 11
    if isinstance(value, datetime.datetime):
225 11
        if iso:
226 11
            return value.isoformat()
227 11
        return value.strftime('%Y-%m-%dT%H:%M:%S%Z')
228 11
    return value
229

230

231 11
def _get_client_creator(session, region_name):
232 11
    def client_creator(service_name, **kwargs):
233 11
        create_client_kwargs = {
234
            'region_name': region_name
235
        }
236 11
        create_client_kwargs.update(**kwargs)
237 11
        return session.create_client(service_name, **create_client_kwargs)
238

239 11
    return client_creator
240

241

242 11
def create_assume_role_refresher(client, params):
243 11
    def refresh():
244 11
        response = client.assume_role(**params)
245 11
        credentials = response['Credentials']
246
        # We need to normalize the credential names to
247
        # the values expected by the refresh creds.
248 11
        return {
249
            'access_key': credentials['AccessKeyId'],
250
            'secret_key': credentials['SecretAccessKey'],
251
            'token': credentials['SessionToken'],
252
            'expiry_time': _serialize_if_needed(credentials['Expiration']),
253
        }
254 11
    return refresh
255

256

257 11
def create_mfa_serial_refresher(actual_refresh):
258

259 11
    class _Refresher(object):
260 11
        def __init__(self, refresh):
261 11
            self._refresh = refresh
262 11
            self._has_been_called = False
263

264 11
        def __call__(self):
265 11
            if self._has_been_called:
266
                # We can explore an option in the future to support
267
                # reprompting for MFA, but for now we just error out
268
                # when the temp creds expire.
269 11
                raise RefreshWithMFAUnsupportedError()
270 11
            self._has_been_called = True
271 11
            return self._refresh()
272

273 11
    return _Refresher(actual_refresh)
274

275

276 11
class JSONFileCache(object):
277
    """JSON file cache.
278
    This provides a dict like interface that stores JSON serializable
279
    objects.
280
    The objects are serialized to JSON and stored in a file.  These
281
    values can be retrieved at a later time.
282
    """
283

284 11
    CACHE_DIR = os.path.expanduser(os.path.join('~', '.aws', 'boto', 'cache'))
285

286 11
    def __init__(self, working_dir=CACHE_DIR):
287 11
        self._working_dir = working_dir
288

289 11
    def __contains__(self, cache_key):
290 11
        actual_key = self._convert_cache_key(cache_key)
291 11
        return os.path.isfile(actual_key)
292

293 11
    def __getitem__(self, cache_key):
294
        """Retrieve value from a cache key."""
295 11
        actual_key = self._convert_cache_key(cache_key)
296 11
        try:
297 11
            with open(actual_key) as f:
298 11
                return json.load(f)
299 11
        except (OSError, ValueError, IOError):
300 11
            raise KeyError(cache_key)
301

302 11
    def __setitem__(self, cache_key, value):
303 11
        full_key = self._convert_cache_key(cache_key)
304 11
        try:
305 11
            file_content = json.dumps(value, default=_serialize_if_needed)
306 11
        except (TypeError, ValueError):
307 11
            raise ValueError("Value cannot be cached, must be "
308
                             "JSON serializable: %s" % value)
309 11
        if not os.path.isdir(self._working_dir):
310 11
            os.makedirs(self._working_dir)
311 11
        with os.fdopen(os.open(full_key,
312
                               os.O_WRONLY | os.O_CREAT, 0o600), 'w') as f:
313 11
            f.truncate()
314 11
            f.write(file_content)
315

316 11
    def _convert_cache_key(self, cache_key):
317 11
        full_path = os.path.join(self._working_dir, cache_key + '.json')
318 11
        return full_path
319

320

321 11
class Credentials(object):
322
    """
323
    Holds the credentials needed to authenticate requests.
324

325
    :ivar access_key: The access key part of the credentials.
326
    :ivar secret_key: The secret key part of the credentials.
327
    :ivar token: The security token, valid only for session credentials.
328
    :ivar method: A string which identifies where the credentials
329
        were found.
330
    """
331

332 11
    def __init__(self, access_key, secret_key, token=None,
333
                 method=None):
334 11
        self.access_key = access_key
335 11
        self.secret_key = secret_key
336 11
        self.token = token
337

338 11
        if method is None:
339 11
            method = 'explicit'
340 11
        self.method = method
341

342 11
        self._normalize()
343

344 11
    def _normalize(self):
345
        # Keys would sometimes (accidentally) contain non-ascii characters.
346
        # It would cause a confusing UnicodeDecodeError in Python 2.
347
        # We explicitly convert them into unicode to avoid such error.
348
        #
349
        # Eventually the service will decide whether to accept the credential.
350
        # This also complies with the behavior in Python 3.
351 11
        self.access_key = botocore.compat.ensure_unicode(self.access_key)
352 11
        self.secret_key = botocore.compat.ensure_unicode(self.secret_key)
353

354 11
    def get_frozen_credentials(self):
355 11
        return ReadOnlyCredentials(self.access_key,
356
                                   self.secret_key,
357
                                   self.token)
358

359

360 11
class RefreshableCredentials(Credentials):
361
    """
362
    Holds the credentials needed to authenticate requests. In addition, it
363
    knows how to refresh itself.
364

365
    :ivar access_key: The access key part of the credentials.
366
    :ivar secret_key: The secret key part of the credentials.
367
    :ivar token: The security token, valid only for session credentials.
368
    :ivar method: A string which identifies where the credentials
369
        were found.
370
    """
371
    # The time at which we'll attempt to refresh, but not
372
    # block if someone else is refreshing.
373 11
    _advisory_refresh_timeout = 15 * 60
374
    # The time at which all threads will block waiting for
375
    # refreshed credentials.
376 11
    _mandatory_refresh_timeout = 10 * 60
377

378 11
    def __init__(self, access_key, secret_key, token,
379
                 expiry_time, refresh_using, method,
380
                 time_fetcher=_local_now):
381 11
        self._refresh_using = refresh_using
382 11
        self._access_key = access_key
383 11
        self._secret_key = secret_key
384 11
        self._token = token
385 11
        self._expiry_time = expiry_time
386 11
        self._time_fetcher = time_fetcher
387 11
        self._refresh_lock = threading.Lock()
388 11
        self.method = method
389 11
        self._frozen_credentials = ReadOnlyCredentials(
390
            access_key, secret_key, token)
391 11
        self._normalize()
392

393 11
    def _normalize(self):
394 11
        self._access_key = botocore.compat.ensure_unicode(self._access_key)
395 11
        self._secret_key = botocore.compat.ensure_unicode(self._secret_key)
396

397 11
    @classmethod
398 2
    def create_from_metadata(cls, metadata, refresh_using, method):
399 11
        instance = cls(
400
            access_key=metadata['access_key'],
401
            secret_key=metadata['secret_key'],
402
            token=metadata['token'],
403
            expiry_time=cls._expiry_datetime(metadata['expiry_time']),
404
            method=method,
405
            refresh_using=refresh_using
406
        )
407 11
        return instance
408

409 11
    @property
410 2
    def access_key(self):
411
        """Warning: Using this property can lead to race conditions if you
412
        access another property subsequently along the refresh boundary.
413
        Please use get_frozen_credentials instead.
414
        """
415 11
        self._refresh()
416 11
        return self._access_key
417

418 11
    @access_key.setter
419 2
    def access_key(self, value):
420 11
        self._access_key = value
421

422 11
    @property
423 2
    def secret_key(self):
424
        """Warning: Using this property can lead to race conditions if you
425
        access another property subsequently along the refresh boundary.
426
        Please use get_frozen_credentials instead.
427
        """
428 11
        self._refresh()
429 11
        return self._secret_key
430

431 11
    @secret_key.setter
432 2
    def secret_key(self, value):
433 11
        self._secret_key = value
434

435 11
    @property
436 2
    def token(self):
437
        """Warning: Using this property can lead to race conditions if you
438
        access another property subsequently along the refresh boundary.
439
        Please use get_frozen_credentials instead.
440
        """
441 11
        self._refresh()
442 11
        return self._token
443

444 11
    @token.setter
445 2
    def token(self, value):
446 11
        self._token = value
447

448 11
    def _seconds_remaining(self):
449 11
        delta = self._expiry_time - self._time_fetcher()
450 11
        return total_seconds(delta)
451

452 11
    def refresh_needed(self, refresh_in=None):
453
        """Check if a refresh is needed.
454

455
        A refresh is needed if the expiry time associated
456
        with the temporary credentials is less than the
457
        provided ``refresh_in``.  If ``time_delta`` is not
458
        provided, ``self.advisory_refresh_needed`` will be used.
459

460
        For example, if your temporary credentials expire
461
        in 10 minutes and the provided ``refresh_in`` is
462
        ``15 * 60``, then this function will return ``True``.
463

464
        :type refresh_in: int
465
        :param refresh_in: The number of seconds before the
466
            credentials expire in which refresh attempts should
467
            be made.
468

469
        :return: True if refresh needed, False otherwise.
470

471
        """
472 11
        if self._expiry_time is None:
473
            # No expiration, so assume we don't need to refresh.
474 11
            return False
475

476 11
        if refresh_in is None:
477 11
            refresh_in = self._advisory_refresh_timeout
478
        # The credentials should be refreshed if they're going to expire
479
        # in less than 5 minutes.
480 11
        if self._seconds_remaining() >= refresh_in:
481
            # There's enough time left. Don't refresh.
482 11
            return False
483 11
        logger.debug("Credentials need to be refreshed.")
484 11
        return True
485

486 11
    def _is_expired(self):
487
        # Checks if the current credentials are expired.
488 11
        return self.refresh_needed(refresh_in=0)
489

490 11
    def _refresh(self):
491
        # In the common case where we don't need a refresh, we
492
        # can immediately exit and not require acquiring the
493
        # refresh lock.
494 11
        if not self.refresh_needed(self._advisory_refresh_timeout):
495 11
            return
496

497
        # acquire() doesn't accept kwargs, but False is indicating
498
        # that we should not block if we can't acquire the lock.
499
        # If we aren't able to acquire the lock, we'll trigger
500
        # the else clause.
501 11
        if self._refresh_lock.acquire(False):
502 11
            try:
503 11
                if not self.refresh_needed(self._advisory_refresh_timeout):
504 9
                    return
505 11
                is_mandatory_refresh = self.refresh_needed(
506
                    self._mandatory_refresh_timeout)
507 11
                self._protected_refresh(is_mandatory=is_mandatory_refresh)
508 11
                return
509
            finally:
510 11
                self._refresh_lock.release()
511 4
        elif self.refresh_needed(self._mandatory_refresh_timeout):
512
            # If we're within the mandatory refresh window,
513
            # we must block until we get refreshed credentials.
514 0
            with self._refresh_lock:
515 0
                if not self.refresh_needed(self._mandatory_refresh_timeout):
516 0
                    return
517 0
                self._protected_refresh(is_mandatory=True)
518

519 11
    def _protected_refresh(self, is_mandatory):
520
        # precondition: this method should only be called if you've acquired
521
        # the self._refresh_lock.
522 11
        try:
523 11
            metadata = self._refresh_using()
524 11
        except Exception as e:
525 11
            period_name = 'mandatory' if is_mandatory else 'advisory'
526 11
            logger.warning("Refreshing temporary credentials failed "
527
                           "during %s refresh period.",
528
                           period_name, exc_info=True)
529 11
            if is_mandatory:
530
                # If this is a mandatory refresh, then
531
                # all errors that occur when we attempt to refresh
532
                # credentials are propagated back to the user.
533 11
                raise
534
            # Otherwise we'll just return.
535
            # The end result will be that we'll use the current
536
            # set of temporary credentials we have.
537 11
            return
538 11
        self._set_from_data(metadata)
539 11
        self._frozen_credentials = ReadOnlyCredentials(
540
            self._access_key, self._secret_key, self._token)
541 11
        if self._is_expired():
542
            # We successfully refreshed credentials but for whatever
543
            # reason, our refreshing function returned credentials
544
            # that are still expired.  In this scenario, the only
545
            # thing we can do is let the user know and raise
546
            # an exception.
547 11
            msg = ("Credentials were refreshed, but the "
548
                   "refreshed credentials are still expired.")
549 11
            logger.warning(msg)
550 11
            raise RuntimeError(msg)
551

552 11
    @staticmethod
553 2
    def _expiry_datetime(time_str):
554 11
        return parse(time_str)
555

556 11
    def _set_from_data(self, data):
557 11
        expected_keys = ['access_key', 'secret_key', 'token', 'expiry_time']
558 11
        if not data:
559 11
            missing_keys = expected_keys
560
        else:
561 11
            missing_keys = [k for k in expected_keys if k not in data]
562

563 11
        if missing_keys:
564 11
            message = "Credential refresh failed, response did not contain: %s"
565 11
            raise CredentialRetrievalError(
566
                provider=self.method,
567
                error_msg=message % ', '.join(missing_keys),
568
            )
569

570 11
        self.access_key = data['access_key']
571 11
        self.secret_key = data['secret_key']
572 11
        self.token = data['token']
573 11
        self._expiry_time = parse(data['expiry_time'])
574 11
        logger.debug("Retrieved credentials will expire at: %s",
575
                     self._expiry_time)
576 11
        self._normalize()
577

578 11
    def get_frozen_credentials(self):
579
        """Return immutable credentials.
580

581
        The ``access_key``, ``secret_key``, and ``token`` properties
582
        on this class will always check and refresh credentials if
583
        needed before returning the particular credentials.
584

585
        This has an edge case where you can get inconsistent
586
        credentials.  Imagine this:
587

588
            # Current creds are "t1"
589
            tmp.access_key  ---> expired? no, so return t1.access_key
590
            # ---- time is now expired, creds need refreshing to "t2" ----
591
            tmp.secret_key  ---> expired? yes, refresh and return t2.secret_key
592

593
        This means we're using the access key from t1 with the secret key
594
        from t2.  To fix this issue, you can request a frozen credential object
595
        which is guaranteed not to change.
596

597
        The frozen credentials returned from this method should be used
598
        immediately and then discarded.  The typical usage pattern would
599
        be::
600

601
            creds = RefreshableCredentials(...)
602
            some_code = SomeSignerObject()
603
            # I'm about to sign the request.
604
            # The frozen credentials are only used for the
605
            # duration of generate_presigned_url and will be
606
            # immediately thrown away.
607
            request = some_code.sign_some_request(
608
                with_credentials=creds.get_frozen_credentials())
609
            print("Signed request:", request)
610

611
        """
612 11
        self._refresh()
613 11
        return self._frozen_credentials
614

615

616 11
class DeferredRefreshableCredentials(RefreshableCredentials):
617
    """Refreshable credentials that don't require initial credentials.
618

619
    refresh_using will be called upon first access.
620
    """
621 11
    def __init__(self, refresh_using, method, time_fetcher=_local_now):
622 11
        self._refresh_using = refresh_using
623 11
        self._access_key = None
624 11
        self._secret_key = None
625 11
        self._token = None
626 11
        self._expiry_time = None
627 11
        self._time_fetcher = time_fetcher
628 11
        self._refresh_lock = threading.Lock()
629 11
        self.method = method
630 11
        self._frozen_credentials = None
631

632 11
    def refresh_needed(self, refresh_in=None):
633 11
        if self._frozen_credentials is None:
634 11
            return True
635 11
        return super(DeferredRefreshableCredentials, self).refresh_needed(
636
            refresh_in
637
        )
638

639

640 11
class CachedCredentialFetcher(object):
641 11
    DEFAULT_EXPIRY_WINDOW_SECONDS = 60 * 15
642

643 11
    def __init__(self, cache=None, expiry_window_seconds=None):
644 11
        if cache is None:
645 11
            cache = {}
646 11
        self._cache = cache
647 11
        self._cache_key = self._create_cache_key()
648 11
        if expiry_window_seconds is None:
649 11
            expiry_window_seconds = self.DEFAULT_EXPIRY_WINDOW_SECONDS
650 11
        self._expiry_window_seconds = expiry_window_seconds
651

652 11
    def _create_cache_key(self):
653
        raise NotImplementedError('_create_cache_key()')
654

655 11
    def _make_file_safe(self, filename):
656
        # Replace :, path sep, and / to make it the string filename safe.
657 11
        filename = filename.replace(':', '_').replace(os.path.sep, '_')
658 11
        return filename.replace('/', '_')
659

660 11
    def _get_credentials(self):
661
        raise NotImplementedError('_get_credentials()')
662

663 11
    def fetch_credentials(self):
664 11
        return self._get_cached_credentials()
665

666 11
    def _get_cached_credentials(self):
667
        """Get up-to-date credentials.
668

669
        This will check the cache for up-to-date credentials, calling assume
670
        role if none are available.
671
        """
672 11
        response = self._load_from_cache()
673 11
        if response is None:
674 11
            response = self._get_credentials()
675 11
            self._write_to_cache(response)
676
        else:
677 11
            logger.debug("Credentials for role retrieved from cache.")
678

679 11
        creds = response['Credentials']
680 11
        expiration = _serialize_if_needed(creds['Expiration'], iso=True)
681 11
        return {
682
            'access_key': creds['AccessKeyId'],
683
            'secret_key': creds['SecretAccessKey'],
684
            'token': creds['SessionToken'],
685
            'expiry_time': expiration,
686
        }
687

688 11
    def _load_from_cache(self):
689 11
        if self._cache_key in self._cache:
690 11
            creds = deepcopy(self._cache[self._cache_key])
691 11
            if not self._is_expired(creds):
692 11
                return creds
693
            else:
694 11
                logger.debug(
695
                    "Credentials were found in cache, but they are expired."
696
                )
697 11
        return None
698

699 11
    def _write_to_cache(self, response):
700 11
        self._cache[self._cache_key] = deepcopy(response)
701

702 11
    def _is_expired(self, credentials):
703
        """Check if credentials are expired."""
704 11
        end_time = _parse_if_needed(credentials['Credentials']['Expiration'])
705 11
        seconds = total_seconds(end_time - _local_now())
706 11
        return seconds < self._expiry_window_seconds
707

708

709 11
class BaseAssumeRoleCredentialFetcher(CachedCredentialFetcher):
710 11
    def __init__(self, client_creator, role_arn, extra_args=None,
711
                 cache=None, expiry_window_seconds=None):
712 11
        self._client_creator = client_creator
713 11
        self._role_arn = role_arn
714

715 11
        if extra_args is None:
716 11
            self._assume_kwargs = {}
717
        else:
718 11
            self._assume_kwargs = deepcopy(extra_args)
719 11
        self._assume_kwargs['RoleArn'] = self._role_arn
720

721 11
        self._role_session_name = self._assume_kwargs.get('RoleSessionName')
722 11
        self._using_default_session_name = False
723 11
        if not self._role_session_name:
724 11
            self._generate_assume_role_name()
725

726 11
        super(BaseAssumeRoleCredentialFetcher, self).__init__(
727
            cache, expiry_window_seconds
728
        )
729

730 11
    def _generate_assume_role_name(self):
731 11
        self._role_session_name = 'botocore-session-%s' % (int(time.time()))
732 11
        self._assume_kwargs['RoleSessionName'] = self._role_session_name
733 11
        self._using_default_session_name = True
734

735 11
    def _create_cache_key(self):
736
        """Create a predictable cache key for the current configuration.
737

738
        The cache key is intended to be compatible with file names.
739
        """
740 11
        args = deepcopy(self._assume_kwargs)
741

742
        # The role session name gets randomly generated, so we don't want it
743
        # in the hash.
744 11
        if self._using_default_session_name:
745 11
            del args['RoleSessionName']
746

747 11
        if 'Policy' in args:
748
            # To have a predictable hash, the keys of the policy must be
749
            # sorted, so we have to load it here to make sure it gets sorted
750
            # later on.
751 11
            args['Policy'] = json.loads(args['Policy'])
752

753 11
        args = json.dumps(args, sort_keys=True)
754 11
        argument_hash = sha1(args.encode('utf-8')).hexdigest()
755 11
        return self._make_file_safe(argument_hash)
756

757

758 11
class AssumeRoleCredentialFetcher(BaseAssumeRoleCredentialFetcher):
759 11
    def __init__(self, client_creator, source_credentials, role_arn,
760
                 extra_args=None, mfa_prompter=None, cache=None,
761
                 expiry_window_seconds=None):
762
        """
763
        :type client_creator: callable
764
        :param client_creator: A callable that creates a client taking
765
            arguments like ``Session.create_client``.
766

767
        :type source_credentials: Credentials
768
        :param source_credentials: The credentials to use to create the
769
            client for the call to AssumeRole.
770

771
        :type role_arn: str
772
        :param role_arn: The ARN of the role to be assumed.
773

774
        :type extra_args: dict
775
        :param extra_args: Any additional arguments to add to the assume
776
            role request using the format of the botocore operation.
777
            Possible keys include, but may not be limited to,
778
            DurationSeconds, Policy, SerialNumber, ExternalId and
779
            RoleSessionName.
780

781
        :type mfa_prompter: callable
782
        :param mfa_prompter: A callable that returns input provided by the
783
            user (i.e raw_input, getpass.getpass, etc.).
784

785
        :type cache: dict
786
        :param cache: An object that supports ``__getitem__``,
787
            ``__setitem__``, and ``__contains__``.  An example of this is
788
            the ``JSONFileCache`` class in aws-cli.
789

790
        :type expiry_window_seconds: int
791
        :param expiry_window_seconds: The amount of time, in seconds,
792
        """
793 11
        self._source_credentials = source_credentials
794 11
        self._mfa_prompter = mfa_prompter
795 11
        if self._mfa_prompter is None:
796 11
            self._mfa_prompter = getpass.getpass
797

798 11
        super(AssumeRoleCredentialFetcher, self).__init__(
799
            client_creator, role_arn, extra_args=extra_args,
800
            cache=cache, expiry_window_seconds=expiry_window_seconds
801
        )
802

803 11
    def _get_credentials(self):
804
        """Get credentials by calling assume role."""
805 11
        kwargs = self._assume_role_kwargs()
806 11
        client = self._create_client()
807 11
        return client.assume_role(**kwargs)
808

809 11
    def _assume_role_kwargs(self):
810
        """Get the arguments for assume role based on current configuration."""
811 11
        assume_role_kwargs = deepcopy(self._assume_kwargs)
812

813 11
        mfa_serial = assume_role_kwargs.get('SerialNumber')
814

815 11
        if mfa_serial is not None:
816 11
            prompt = 'Enter MFA code for %s: ' % mfa_serial
817 11
            token_code = self._mfa_prompter(prompt)
818 11
            assume_role_kwargs['TokenCode'] = token_code
819

820 11
        duration_seconds = assume_role_kwargs.get('DurationSeconds')
821

822 11
        if duration_seconds is not None:
823 11
            assume_role_kwargs['DurationSeconds'] = duration_seconds
824

825 11
        return assume_role_kwargs
826

827 11
    def _create_client(self):
828
        """Create an STS client using the source credentials."""
829 11
        frozen_credentials = self._source_credentials.get_frozen_credentials()
830 11
        return self._client_creator(
831
            'sts',
832
            aws_access_key_id=frozen_credentials.access_key,
833
            aws_secret_access_key=frozen_credentials.secret_key,
834
            aws_session_token=frozen_credentials.token,
835
        )
836

837

838 11
class AssumeRoleWithWebIdentityCredentialFetcher(
839
        BaseAssumeRoleCredentialFetcher
840
):
841 11
    def __init__(self, client_creator, web_identity_token_loader, role_arn,
842
                 extra_args=None, cache=None, expiry_window_seconds=None):
843
        """
844
        :type client_creator: callable
845
        :param client_creator: A callable that creates a client taking
846
            arguments like ``Session.create_client``.
847

848
        :type web_identity_token_loader: callable
849
        :param web_identity_token_loader: A callable that takes no arguments
850
        and returns a web identity token str.
851

852
        :type role_arn: str
853
        :param role_arn: The ARN of the role to be assumed.
854

855
        :type extra_args: dict
856
        :param extra_args: Any additional arguments to add to the assume
857
            role request using the format of the botocore operation.
858
            Possible keys include, but may not be limited to,
859
            DurationSeconds, Policy, SerialNumber, ExternalId and
860
            RoleSessionName.
861

862
        :type cache: dict
863
        :param cache: An object that supports ``__getitem__``,
864
            ``__setitem__``, and ``__contains__``.  An example of this is
865
            the ``JSONFileCache`` class in aws-cli.
866

867
        :type expiry_window_seconds: int
868
        :param expiry_window_seconds: The amount of time, in seconds,
869
        """
870 11
        self._web_identity_token_loader = web_identity_token_loader
871

872 11
        super(AssumeRoleWithWebIdentityCredentialFetcher, self).__init__(
873
            client_creator, role_arn, extra_args=extra_args,
874
            cache=cache, expiry_window_seconds=expiry_window_seconds
875
        )
876

877 11
    def _get_credentials(self):
878
        """Get credentials by calling assume role."""
879 11
        kwargs = self._assume_role_kwargs()
880
        # Assume role with web identity does not require credentials other than
881
        # the token, explicitly configure the client to not sign requests.
882 11
        config = Config(signature_version=UNSIGNED)
883 11
        client = self._client_creator('sts', config=config)
884 11
        return client.assume_role_with_web_identity(**kwargs)
885

886 11
    def _assume_role_kwargs(self):
887
        """Get the arguments for assume role based on current configuration."""
888 11
        assume_role_kwargs = deepcopy(self._assume_kwargs)
889 11
        identity_token = self._web_identity_token_loader()
890 11
        assume_role_kwargs['WebIdentityToken'] = identity_token
891

892 11
        return assume_role_kwargs
893

894

895 11
class CredentialProvider(object):
896
    # A short name to identify the provider within botocore.
897 11
    METHOD = None
898

899
    # A name to identify the provider for use in cross-sdk features like
900
    # assume role's `credential_source` configuration option. These names
901
    # are to be treated in a case-insensitive way. NOTE: any providers not
902
    # implemented in botocore MUST prefix their canonical names with
903
    # 'custom' or we DO NOT guarantee that it will work with any features
904
    # that this provides.
905 11
    CANONICAL_NAME = None
906

907 11
    def __init__(self, session=None):
908 0
        self.session = session
909

910 11
    def load(self):
911
        """
912
        Loads the credentials from their source & sets them on the object.
913

914
        Subclasses should implement this method (by reading from disk, the
915
        environment, the network or wherever), returning ``True`` if they were
916
        found & loaded.
917

918
        If not found, this method should return ``False``, indictating that the
919
        ``CredentialResolver`` should fall back to the next available method.
920

921
        The default implementation does nothing, assuming the user has set the
922
        ``access_key/secret_key/token`` themselves.
923

924
        :returns: Whether credentials were found & set
925
        :rtype: Credentials
926
        """
927 0
        return True
928

929 11
    def _extract_creds_from_mapping(self, mapping, *key_names):
930 11
        found = []
931 11
        for key_name in key_names:
932 11
            try:
933 11
                found.append(mapping[key_name])
934 11
            except KeyError:
935 11
                raise PartialCredentialsError(provider=self.METHOD,
936
                                              cred_var=key_name)
937 11
        return found
938

939

940 11
class ProcessProvider(CredentialProvider):
941

942 11
    METHOD = 'custom-process'
943

944 11
    def __init__(self, profile_name, load_config, popen=subprocess.Popen):
945 11
        self._profile_name = profile_name
946 11
        self._load_config = load_config
947 11
        self._loaded_config = None
948 11
        self._popen = popen
949

950 11
    def load(self):
951 11
        credential_process = self._credential_process
952 11
        if credential_process is None:
953 11
            return
954

955 11
        creds_dict = self._retrieve_credentials_using(credential_process)
956 11
        if creds_dict.get('expiry_time') is not None:
957 11
            return RefreshableCredentials.create_from_metadata(
958
                creds_dict,
959
                lambda: self._retrieve_credentials_using(credential_process),
960
                self.METHOD
961
            )
962

963 11
        return Credentials(
964
            access_key=creds_dict['access_key'],
965
            secret_key=creds_dict['secret_key'],
966
            token=creds_dict.get('token'),
967
            method=self.METHOD
968
        )
969

970 11
    def _retrieve_credentials_using(self, credential_process):
971
        # We're not using shell=True, so we need to pass the
972
        # command and all arguments as a list.
973 11
        process_list = compat_shell_split(credential_process)
974 11
        p = self._popen(process_list,
975
                        stdout=subprocess.PIPE,
976
                        stderr=subprocess.PIPE)
977 11
        stdout, stderr = p.communicate()
978 11
        if p.returncode != 0:
979 11
            raise CredentialRetrievalError(
980
                provider=self.METHOD, error_msg=stderr.decode('utf-8'))
981 11
        parsed = botocore.compat.json.loads(stdout.decode('utf-8'))
982 11
        version = parsed.get('Version', '<Version key not provided>')
983 11
        if version != 1:
984 11
            raise CredentialRetrievalError(
985
                provider=self.METHOD,
986
                error_msg=("Unsupported version '%s' for credential process "
987
                           "provider, supported versions: 1" % version))
988 11
        try:
989 11
            return {
990
                'access_key': parsed['AccessKeyId'],
991
                'secret_key': parsed['SecretAccessKey'],
992
                'token': parsed.get('SessionToken'),
993
                'expiry_time': parsed.get('Expiration'),
994
            }
995 11
        except KeyError as e:
996 11
            raise CredentialRetrievalError(
997
                provider=self.METHOD,
998
                error_msg="Missing required key in response: %s" % e
999
            )
1000

1001 11
    @property
1002 2
    def _credential_process(self):
1003 11
        if self._loaded_config is None:
1004 11
            self._loaded_config = self._load_config()
1005 11
        profile_config = self._loaded_config.get(
1006
            'profiles', {}).get(self._profile_name, {})
1007 11
        return profile_config.get('credential_process')
1008

1009

1010 11
class InstanceMetadataProvider(CredentialProvider):
1011 11
    METHOD = 'iam-role'
1012 11
    CANONICAL_NAME = 'Ec2InstanceMetadata'
1013

1014 11
    def __init__(self, iam_role_fetcher):
1015 11
        self._role_fetcher = iam_role_fetcher
1016

1017 11
    def load(self):
1018 11
        fetcher = self._role_fetcher
1019
        # We do the first request, to see if we get useful data back.
1020
        # If not, we'll pass & move on to whatever's next in the credential
1021
        # chain.
1022 11
        metadata = fetcher.retrieve_iam_role_credentials()
1023 11
        if not metadata:
1024 11
            return None
1025 11
        logger.debug('Found credentials from IAM Role: %s',
1026
                     metadata['role_name'])
1027
        # We manually set the data here, since we already made the request &
1028
        # have it. When the expiry is hit, the credentials will auto-refresh
1029
        # themselves.
1030 11
        creds = RefreshableCredentials.create_from_metadata(
1031
            metadata,
1032
            method=self.METHOD,
1033
            refresh_using=fetcher.retrieve_iam_role_credentials,
1034
        )
1035 11
        return creds
1036

1037

1038 11
class EnvProvider(CredentialProvider):
1039 11
    METHOD = 'env'
1040 11
    CANONICAL_NAME = 'Environment'
1041 11
    ACCESS_KEY = 'AWS_ACCESS_KEY_ID'
1042 11
    SECRET_KEY = 'AWS_SECRET_ACCESS_KEY'
1043
    # The token can come from either of these env var.
1044
    # AWS_SESSION_TOKEN is what other AWS SDKs have standardized on.
1045 11
    TOKENS = ['AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN']
1046 11
    EXPIRY_TIME = 'AWS_CREDENTIAL_EXPIRATION'
1047

1048 11
    def __init__(self, environ=None, mapping=None):
1049
        """
1050

1051
        :param environ: The environment variables (defaults to
1052
            ``os.environ`` if no value is provided).
1053
        :param mapping: An optional mapping of variable names to
1054
            environment variable names.  Use this if you want to
1055
            change the mapping of access_key->AWS_ACCESS_KEY_ID, etc.
1056
            The dict can have up to 3 keys: ``access_key``, ``secret_key``,
1057
            ``session_token``.
1058
        """
1059 11
        if environ is None:
1060 11
            environ = os.environ
1061 11
        self.environ = environ
1062 11
        self._mapping = self._build_mapping(mapping)
1063

1064 11
    def _build_mapping(self, mapping):
1065
        # Mapping of variable name to env var name.
1066 11
        var_mapping = {}
1067 11
        if mapping is None:
1068
            # Use the class var default.
1069 11
            var_mapping['access_key'] = self.ACCESS_KEY
1070 11
            var_mapping['secret_key'] = self.SECRET_KEY
1071 11
            var_mapping['token'] = self.TOKENS
1072 11
            var_mapping['expiry_time'] = self.EXPIRY_TIME
1073
        else:
1074 11
            var_mapping['access_key'] = mapping.get(
1075
                'access_key', self.ACCESS_KEY)
1076 11
            var_mapping['secret_key'] = mapping.get(
1077
                'secret_key', self.SECRET_KEY)
1078 11
            var_mapping['token'] = mapping.get(
1079
                'token', self.TOKENS)
1080 11
            if not isinstance(var_mapping['token'], list):
1081 11
                var_mapping['token'] = [var_mapping['token']]
1082 11
            var_mapping['expiry_time'] = mapping.get(
1083
                'expiry_time', self.EXPIRY_TIME)
1084 11
        return var_mapping
1085

1086 11
    def load(self):
1087
        """
1088
        Search for credentials in explicit environment variables.
1089
        """
1090

1091 11
        access_key = self.environ.get(self._mapping['access_key'], '')
1092

1093 11
        if access_key:
1094 11
            logger.info('Found credentials in environment variables.')
1095 11
            fetcher = self._create_credentials_fetcher()
1096 11
            credentials = fetcher(require_expiry=False)
1097

1098 11
            expiry_time = credentials['expiry_time']
1099 11
            if expiry_time is not None:
1100 11
                expiry_time = parse(expiry_time)
1101 11
                return RefreshableCredentials(
1102
                    credentials['access_key'], credentials['secret_key'],
1103
                    credentials['token'], expiry_time,
1104
                    refresh_using=fetcher, method=self.METHOD
1105
                )
1106

1107 11
            return Credentials(
1108
                credentials['access_key'], credentials['secret_key'],
1109
                credentials['token'], method=self.METHOD
1110
            )
1111
        else:
1112 11
            return None
1113

1114 11
    def _create_credentials_fetcher(self):
1115 11
        mapping = self._mapping
1116 11
        method = self.METHOD
1117 11
        environ = self.environ
1118

1119 11
        def fetch_credentials(require_expiry=True):
1120 11
            credentials = {}
1121

1122 11
            access_key = environ.get(mapping['access_key'], '')
1123 11
            if not access_key:
1124 11
                raise PartialCredentialsError(
1125
                    provider=method, cred_var=mapping['access_key'])
1126 11
            credentials['access_key'] = access_key
1127

1128 11
            secret_key = environ.get(mapping['secret_key'], '')
1129 11
            if not secret_key:
1130 11
                raise PartialCredentialsError(
1131
                    provider=method, cred_var=mapping['secret_key'])
1132 11
            credentials['secret_key'] = secret_key
1133

1134 11
            credentials['token'] = None
1135 11
            for token_env_var in mapping['token']:
1136 11
                token = environ.get(token_env_var, '')
1137 11
                if token:
1138 11
                    credentials['token'] = token
1139 11
                    break
1140

1141 11
            credentials['expiry_time'] = None
1142 11
            expiry_time = environ.get(mapping['expiry_time'], '')
1143 11
            if expiry_time:
1144 11
                credentials['expiry_time'] = expiry_time
1145 11
            if require_expiry and not expiry_time:
1146 11
                raise PartialCredentialsError(
1147
                    provider=method, cred_var=mapping['expiry_time'])
1148

1149 11
            return credentials
1150

1151 11
        return fetch_credentials
1152

1153

1154 11
class OriginalEC2Provider(CredentialProvider):
1155 11
    METHOD = 'ec2-credentials-file'
1156 11
    CANONICAL_NAME = 'Ec2Config'
1157

1158 11
    CRED_FILE_ENV = 'AWS_CREDENTIAL_FILE'
1159 11
    ACCESS_KEY = 'AWSAccessKeyId'
1160 11
    SECRET_KEY = 'AWSSecretKey'
1161

1162 11
    def __init__(self, environ=None, parser=None):
1163 11
        if environ is None:
1164 11
            environ = os.environ
1165 11
        if parser is None:
1166 11
            parser = parse_key_val_file
1167 11
        self._environ = environ
1168 11
        self._parser = parser
1169

1170 11
    def load(self):
1171
        """
1172
        Search for a credential file used by original EC2 CLI tools.
1173
        """
1174 11
        if 'AWS_CREDENTIAL_FILE' in self._environ:
1175 11
            full_path = os.path.expanduser(
1176
                self._environ['AWS_CREDENTIAL_FILE'])
1177 11
            creds = self._parser(full_path)
1178 11
            if self.ACCESS_KEY in creds:
1179 11
                logger.info('Found credentials in AWS_CREDENTIAL_FILE.')
1180 11
                access_key = creds[self.ACCESS_KEY]
1181 11
                secret_key = creds[self.SECRET_KEY]
1182
                # EC2 creds file doesn't support session tokens.
1183 11
                return Credentials(access_key, secret_key, method=self.METHOD)
1184
        else:
1185 11
            return None
1186

1187

1188 11
class SharedCredentialProvider(CredentialProvider):
1189 11
    METHOD = 'shared-credentials-file'
1190 11
    CANONICAL_NAME = 'SharedCredentials'
1191

1192 11
    ACCESS_KEY = 'aws_access_key_id'
1193 11
    SECRET_KEY = 'aws_secret_access_key'
1194
    # Same deal as the EnvProvider above.  Botocore originally supported
1195
    # aws_security_token, but the SDKs are standardizing on aws_session_token
1196
    # so we support both.
1197 11
    TOKENS = ['aws_security_token', 'aws_session_token']
1198

1199 11
    def __init__(self, creds_filename, profile_name=None, ini_parser=None):
1200 11
        self._creds_filename = creds_filename
1201 11
        if profile_name is None:
1202 0
            profile_name = 'default'
1203 11
        self._profile_name = profile_name
1204 11
        if ini_parser is None:
1205 11
            ini_parser = botocore.configloader.raw_config_parse
1206 11
        self._ini_parser = ini_parser
1207

1208 11
    def load(self):
1209 11
        try:
1210 11
            available_creds = self._ini_parser(self._creds_filename)
1211 11
        except ConfigNotFound:
1212 11
            return None
1213 11
        if self._profile_name in available_creds:
1214 11
            config = available_creds[self._profile_name]
1215 11
            if self.ACCESS_KEY in config:
1216 11
                logger.info("Found credentials in shared credentials file: %s",
1217
                            self._creds_filename)
1218 11
                access_key, secret_key = self._extract_creds_from_mapping(
1219
                    config, self.ACCESS_KEY, self.SECRET_KEY)
1220 11
                token = self._get_session_token(config)
1221 11
                return Credentials(access_key, secret_key, token,
1222
                                   method=self.METHOD)
1223

1224 11
    def _get_session_token(self, config):
1225 11
        for token_envvar in self.TOKENS:
1226 11
            if token_envvar in config:
1227 11
                return config[token_envvar]
1228

1229

1230 11
class ConfigProvider(CredentialProvider):
1231
    """INI based config provider with profile sections."""
1232 11
    METHOD = 'config-file'
1233 11
    CANONICAL_NAME = 'SharedConfig'
1234

1235 11
    ACCESS_KEY = 'aws_access_key_id'
1236 11
    SECRET_KEY = 'aws_secret_access_key'
1237
    # Same deal as the EnvProvider above.  Botocore originally supported
1238
    # aws_security_token, but the SDKs are standardizing on aws_session_token
1239
    # so we support both.
1240 11
    TOKENS = ['aws_security_token', 'aws_session_token']
1241

1242 11
    def __init__(self, config_filename, profile_name, config_parser=None):
1243
        """
1244

1245
        :param config_filename: The session configuration scoped to the current
1246
            profile.  This is available via ``session.config``.
1247
        :param profile_name: The name of the current profile.
1248
        :param config_parser: A config parser callable.
1249

1250
        """
1251 11
        self._config_filename = config_filename
1252 11
        self._profile_name = profile_name
1253 11
        if config_parser is None:
1254 11
            config_parser = botocore.configloader.load_config
1255 11
        self._config_parser = config_parser
1256

1257 11
    def load(self):
1258
        """
1259
        If there is are credentials in the configuration associated with
1260
        the session, use those.
1261
        """
1262 11
        try:
1263 11
            full_config = self._config_parser(self._config_filename)
1264 11
        except ConfigNotFound:
1265 11
            return None
1266 11
        if self._profile_name in full_config['profiles']:
1267 11
            profile_config = full_config['profiles'][self._profile_name]
1268 11
            if self.ACCESS_KEY in profile_config:
1269 11
                logger.info("Credentials found in config file: %s",
1270
                            self._config_filename)
1271 11
                access_key, secret_key = self._extract_creds_from_mapping(
1272
                    profile_config, self.ACCESS_KEY, self.SECRET_KEY)
1273 11
                token = self._get_session_token(profile_config)
1274 11
                return Credentials(access_key, secret_key, token,
1275
                                   method=self.METHOD)
1276
        else:
1277 11
            return None
1278

1279 11
    def _get_session_token(self, profile_config):
1280 11
        for token_name in self.TOKENS:
1281 11
            if token_name in profile_config:
1282 11
                return profile_config[token_name]
1283

1284

1285 11
class BotoProvider(CredentialProvider):
1286 11
    METHOD = 'boto-config'
1287 11
    CANONICAL_NAME = 'Boto2Config'
1288

1289 11
    BOTO_CONFIG_ENV = 'BOTO_CONFIG'
1290 11
    DEFAULT_CONFIG_FILENAMES = ['/etc/boto.cfg', '~/.boto']
1291 11
    ACCESS_KEY = 'aws_access_key_id'
1292 11
    SECRET_KEY = 'aws_secret_access_key'
1293

1294 11
    def __init__(self, environ=None, ini_parser=None):
1295 11
        if environ is None:
1296 11
            environ = os.environ
1297 11
        if ini_parser is None:
1298 11
            ini_parser = botocore.configloader.raw_config_parse
1299 11
        self._environ = environ
1300 11
        self._ini_parser = ini_parser
1301

1302 11
    def load(self):
1303
        """
1304
        Look for credentials in boto config file.
1305
        """
1306 11
        if self.BOTO_CONFIG_ENV in self._environ:
1307 11
            potential_locations = [self._environ[self.BOTO_CONFIG_ENV]]
1308
        else:
1309 11
            potential_locations = self.DEFAULT_CONFIG_FILENAMES
1310 11
        for filename in potential_locations:
1311 11
            try:
1312 11
                config = self._ini_parser(filename)
1313 11
            except ConfigNotFound:
1314
                # Move on to the next potential config file name.
1315 11
                continue
1316 11
            if 'Credentials' in config:
1317 11
                credentials = config['Credentials']
1318 11
                if self.ACCESS_KEY in credentials:
1319 11
                    logger.info("Found credentials in boto config file: %s",
1320
                                filename)
1321 11
                    access_key, secret_key = self._extract_creds_from_mapping(
1322
                        credentials, self.ACCESS_KEY, self.SECRET_KEY)
1323 11
                    return Credentials(access_key, secret_key,
1324
                                       method=self.METHOD)
1325

1326

1327 11
class AssumeRoleProvider(CredentialProvider):
1328 11
    METHOD = 'assume-role'
1329
    # The AssumeRole provider is logically part of the SharedConfig and
1330
    # SharedCredentials providers. Since the purpose of the canonical name
1331
    # is to provide cross-sdk compatibility, calling code will need to be
1332
    # aware that either of those providers should be tied to the AssumeRole
1333
    # provider as much as possible.
1334 11
    CANONICAL_NAME = None
1335 11
    ROLE_CONFIG_VAR = 'role_arn'
1336 11
    WEB_IDENTITY_TOKE_FILE_VAR = 'web_identity_token_file'
1337
    # Credentials are considered expired (and will be refreshed) once the total
1338
    # remaining time left until the credentials expires is less than the
1339
    # EXPIRY_WINDOW.
1340 11
    EXPIRY_WINDOW_SECONDS = 60 * 15
1341

1342 11
    def __init__(self, load_config, client_creator, cache, profile_name,
1343
                 prompter=getpass.getpass, credential_sourcer=None,
1344
                 profile_provider_builder=None):
1345
        """
1346
        :type load_config: callable
1347
        :param load_config: A function that accepts no arguments, and
1348
            when called, will return the full configuration dictionary
1349
            for the session (``session.full_config``).
1350

1351
        :type client_creator: callable
1352
        :param client_creator: A factory function that will create
1353
            a client when called.  Has the same interface as
1354
            ``botocore.session.Session.create_client``.
1355

1356
        :type cache: dict
1357
        :param cache: An object that supports ``__getitem__``,
1358
            ``__setitem__``, and ``__contains__``.  An example
1359
            of this is the ``JSONFileCache`` class in the CLI.
1360

1361
        :type profile_name: str
1362
        :param profile_name: The name of the profile.
1363

1364
        :type prompter: callable
1365
        :param prompter: A callable that returns input provided
1366
            by the user (i.e raw_input, getpass.getpass, etc.).
1367

1368
        :type credential_sourcer: CanonicalNameCredentialSourcer
1369
        :param credential_sourcer: A credential provider that takes a
1370
            configuration, which is used to provide the source credentials
1371
            for the STS call.
1372
        """
1373
        #: The cache used to first check for assumed credentials.
1374
        #: This is checked before making the AssumeRole API
1375
        #: calls and can be useful if you have short lived
1376
        #: scripts and you'd like to avoid calling AssumeRole
1377
        #: until the credentials are expired.
1378 11
        self.cache = cache
1379 11
        self._load_config = load_config
1380
        # client_creator is a callable that creates function.
1381
        # It's basically session.create_client
1382 11
        self._client_creator = client_creator
1383 11
        self._profile_name = profile_name
1384 11
        self._prompter = prompter
1385
        # The _loaded_config attribute will be populated from the
1386
        # load_config() function once the configuration is actually
1387
        # loaded.  The reason we go through all this instead of just
1388
        # requiring that the loaded_config be passed to us is to that
1389
        # we can defer configuration loaded until we actually try
1390
        # to load credentials (as opposed to when the object is
1391
        # instantiated).
1392 11
        self._loaded_config = {}
1393 11
        self._credential_sourcer = credential_sourcer
1394 11
        self._profile_provider_builder = profile_provider_builder
1395 11
        self._visited_profiles = [self._profile_name]
1396

1397 11
    def load(self):
1398 11
        self._loaded_config = self._load_config()
1399 11
        profiles = self._loaded_config.get('profiles', {})
1400 11
        profile = profiles.get(self._profile_name, {})
1401 11
        if self._has_assume_role_config_vars(profile):
1402 11
            return self._load_creds_via_assume_role(self._profile_name)
1403

1404 11
    def _has_assume_role_config_vars(self, profile):
1405 11
        return (
1406
            self.ROLE_CONFIG_VAR in profile and
1407
            # We need to ensure this provider doesn't look at a profile when
1408
            # the profile has configuration for web identity. Simply relying on
1409
            # the order in the credential chain is insufficient as it doesn't
1410
            # prevent the case when we're doing an assume role chain.
1411
            self.WEB_IDENTITY_TOKE_FILE_VAR not in profile
1412
        )
1413

1414 11
    def _load_creds_via_assume_role(self, profile_name):
1415 11
        role_config = self._get_role_config(profile_name)
1416 11
        source_credentials = self._resolve_source_credentials(
1417
            role_config, profile_name
1418
        )
1419

1420 11
        extra_args = {}
1421 11
        role_session_name = role_config.get('role_session_name')
1422 11
        if role_session_name is not None:
1423 11
            extra_args['RoleSessionName'] = role_session_name
1424

1425 11
        external_id = role_config.get('external_id')
1426 11
        if external_id is not None:
1427 11
            extra_args['ExternalId'] = external_id
1428

1429 11
        mfa_serial = role_config.get('mfa_serial')
1430 11
        if mfa_serial is not None:
1431 11
            extra_args['SerialNumber'] = mfa_serial
1432

1433 11
        duration_seconds = role_config.get('duration_seconds')
1434 11
        if duration_seconds is not None:
1435 11
            extra_args['DurationSeconds'] = duration_seconds
1436

1437 11
        fetcher = AssumeRoleCredentialFetcher(
1438
            client_creator=self._client_creator,
1439
            source_credentials=source_credentials,
1440
            role_arn=role_config['role_arn'],
1441
            extra_args=extra_args,
1442
            mfa_prompter=self._prompter,
1443
            cache=self.cache,
1444
        )
1445 11
        refresher = fetcher.fetch_credentials
1446 11
        if mfa_serial is not None:
1447 11
            refresher = create_mfa_serial_refresher(refresher)
1448

1449
        # The initial credentials are empty and the expiration time is set
1450
        # to now so that we can delay the call to assume role until it is
1451
        # strictly needed.
1452 11
        return DeferredRefreshableCredentials(
1453
            method=self.METHOD,
1454
            refresh_using=refresher,
1455
            time_fetcher=_local_now
1456
        )
1457

1458 11
    def _get_role_config(self, profile_name):
1459
        """Retrieves and validates the role configuration for the profile."""
1460 11
        profiles = self._loaded_config.get('profiles', {})
1461

1462 11
        profile = profiles[profile_name]
1463 11
        source_profile = profile.get('source_profile')
1464 11
        role_arn = profile['role_arn']
1465 11
        credential_source = profile.get('credential_source')
1466 11
        mfa_serial = profile.get('mfa_serial')
1467 11
        external_id = profile.get('external_id')
1468 11
        role_session_name = profile.get('role_session_name')
1469 11
        duration_seconds = profile.get('duration_seconds')
1470

1471 11
        role_config = {
1472
            'role_arn': role_arn,
1473
            'external_id': external_id,
1474
            'mfa_serial': mfa_serial,
1475
            'role_session_name': role_session_name,
1476
            'source_profile': source_profile,
1477
            'credential_source': credential_source
1478
        }
1479

1480 11
        if duration_seconds is not None:
1481 11
          try:
1482 11
            role_config['duration_seconds'] = int(duration_seconds)
1483 11
          except ValueError:
1484 11
            pass
1485

1486
        # Either the credential source or the source profile must be
1487
        # specified, but not both.
1488 11
        if credential_source is not None and source_profile is not None:
1489 11
            raise InvalidConfigError(
1490
                error_msg=(
1491
                    'The profile "%s" contains both source_profile and '
1492
                    'credential_source.' % profile_name
1493
                )
1494
            )
1495 11
        elif credential_source is None and source_profile is None:
1496 11
            raise PartialCredentialsError(
1497
                provider=self.METHOD,
1498
                cred_var='source_profile or credential_source'
1499
            )
1500 11
        elif credential_source is not None:
1501 11
            self._validate_credential_source(
1502
                profile_name, credential_source)
1503
        else:
1504 11
            self._validate_source_profile(profile_name, source_profile)
1505

1506 11
        return role_config
1507

1508 11
    def _validate_credential_source(self, parent_profile, credential_source):
1509 11
        if self._credential_sourcer is None:
1510 11
            raise InvalidConfigError(error_msg=(
1511
                'The credential_source "%s" is specified in profile "%s", '
1512
                'but no source provider was configured.' % (
1513
                    credential_source, parent_profile)
1514
            ))
1515 11
        if not self._credential_sourcer.is_supported(credential_source):
1516 11
            raise InvalidConfigError(error_msg=(
1517
                'The credential source "%s" referenced in profile "%s" is not '
1518
                'valid.' % (credential_source, parent_profile)
1519
            ))
1520

1521 11
    def _source_profile_has_credentials(self, profile):
1522 0
        return any([
1523
            self._has_static_credentials(profile),
1524
            self._has_assume_role_config_vars(profile),
1525
        ])
1526

1527 11
    def _validate_source_profile(self, parent_profile_name,
1528
                                 source_profile_name):
1529 11
        profiles = self._loaded_config.get('profiles', {})
1530 11
        if source_profile_name not in profiles:
1531 11
            raise InvalidConfigError(
1532
                error_msg=(
1533
                    'The source_profile "%s" referenced in '
1534
                    'the profile "%s" does not exist.' % (
1535
                        source_profile_name, parent_profile_name)
1536
                )
1537
            )
1538

1539 11
        source_profile = profiles[source_profile_name]
1540

1541
        # Make sure we aren't going into an infinite loop. If we haven't
1542
        # visited the profile yet, we're good.
1543 11
        if source_profile_name not in self._visited_profiles:
1544 11
            return
1545

1546
        # If we have visited the profile and the profile isn't simply
1547
        # referencing itself, that's an infinite loop.
1548 11
        if source_profile_name != parent_profile_name:
1549 11
            raise InfiniteLoopConfigError(
1550
                source_profile=source_profile_name,
1551
                visited_profiles=self._visited_profiles
1552
            )
1553

1554
        # A profile is allowed to reference itself so that it can source
1555
        # static credentials and have configuration all in the same
1556
        # profile. This will only ever work for the top level assume
1557
        # role because the static credentials will otherwise take
1558
        # precedence.
1559 11
        if not self._has_static_credentials(source_profile):
1560 11
            raise InfiniteLoopConfigError(
1561
                source_profile=source_profile_name,
1562
                visited_profiles=self._visited_profiles
1563
            )
1564

1565 11
    def _has_static_credentials(self, profile):
1566 11
        static_keys = ['aws_secret_access_key', 'aws_access_key_id']
1567 11
        return any(static_key in profile for static_key in static_keys)
1568

1569 11
    def _resolve_source_credentials(self, role_config, profile_name):
1570 11
        credential_source = role_config.get('credential_source')
1571 11
        if credential_source is not None:
1572 11
            return self._resolve_credentials_from_source(
1573
                credential_source, profile_name
1574
            )
1575

1576 11
        source_profile = role_config['source_profile']
1577 11
        self._visited_profiles.append(source_profile)
1578 11
        return self._resolve_credentials_from_profile(source_profile)
1579

1580 11
    def _resolve_credentials_from_profile(self, profile_name):
1581 11
        profiles = self._loaded_config.get('profiles', {})
1582 11
        profile = profiles[profile_name]
1583

1584 11
        if self._has_static_credentials(profile) and \
1585
                not self._profile_provider_builder:
1586
            # This is only here for backwards compatibility. If this provider
1587
            # isn't given a profile provider builder we still want to be able
1588
            # handle the basic static credential case as we would before the
1589
            # provile provider builder parameter was added.
1590 11
            return self._resolve_static_credentials_from_profile(profile)
1591 11
        elif self._has_static_credentials(profile) or \
1592
                not self._has_assume_role_config_vars(profile):
1593 11
            profile_providers = self._profile_provider_builder.providers(
1594
                profile_name=profile_name,
1595
                disable_env_vars=True,
1596
            )
1597 11
            profile_chain = CredentialResolver(profile_providers)
1598 11
            credentials = profile_chain.load_credentials()
1599 11
            if credentials is None:
1600 11
                error_message = (
1601
                    'The source profile "%s" must have credentials.'
1602
                )
1603 11
                raise InvalidConfigError(
1604
                    error_msg=error_message % profile_name,
1605
                )
1606 11
            return credentials
1607

1608 11
        return self._load_creds_via_assume_role(profile_name)
1609

1610 11
    def _resolve_static_credentials_from_profile(self, profile):
1611 11
        try:
1612 11
            return Credentials(
1613
                access_key=profile['aws_access_key_id'],
1614
                secret_key=profile['aws_secret_access_key'],
1615
                token=profile.get('aws_session_token')
1616
            )
1617 11
        except KeyError as e:
1618 11
            raise PartialCredentialsError(
1619
                provider=self.METHOD, cred_var=str(e))
1620

1621 11
    def _resolve_credentials_from_source(self, credential_source,
1622
                                         profile_name):
1623 11
        credentials = self._credential_sourcer.source_credentials(
1624
            credential_source)
1625 11
        if credentials is None:
1626 11
            raise CredentialRetrievalError(
1627
                provider=credential_source,
1628
                error_msg=(
1629
                    'No credentials found in credential_source referenced '
1630
                    'in profile %s' % profile_name
1631
                )
1632
            )
1633 11
        return credentials
1634

1635

1636 11
class AssumeRoleWithWebIdentityProvider(CredentialProvider):
1637 11
    METHOD = 'assume-role-with-web-identity'
1638 11
    CANONICAL_NAME = None
1639 11
    _CONFIG_TO_ENV_VAR = {
1640
        'web_identity_token_file': 'AWS_WEB_IDENTITY_TOKEN_FILE',
1641
        'role_session_name': 'AWS_ROLE_SESSION_NAME',
1642
        'role_arn': 'AWS_ROLE_ARN',
1643
    }
1644

1645 11
    def __init__(
1646
            self,
1647
            load_config,
1648
            client_creator,
1649
            profile_name,
1650
            cache=None,
1651
            disable_env_vars=False,
1652
            token_loader_cls=None,
1653
    ):
1654 11
        self.cache = cache
1655 11
        self._load_config = load_config
1656 11
        self._client_creator = client_creator
1657 11
        self._profile_name = profile_name
1658 11
        self._profile_config = None
1659 11
        self._disable_env_vars = disable_env_vars
1660 11
        if token_loader_cls is None:
1661 11
            token_loader_cls = FileWebIdentityTokenLoader
1662 11
        self._token_loader_cls = token_loader_cls
1663

1664 11
    def load(self):
1665 11
        return self._assume_role_with_web_identity()
1666

1667 11
    def _get_profile_config(self, key):
1668 11
        if self._profile_config is None:
1669 11
            loaded_config = self._load_config()
1670 11
            profiles = loaded_config.get('profiles', {})
1671 11
            self._profile_config = profiles.get(self._profile_name, {})
1672 11
        return self._profile_config.get(key)
1673

1674 11
    def _get_env_config(self, key):
1675 11
        if self._disable_env_vars:
1676 11
            return None
1677 11
        env_key = self._CONFIG_TO_ENV_VAR.get(key)
1678 11
        if env_key and env_key in os.environ:
1679 11
            return os.environ[env_key]
1680 11
        return None
1681

1682 11
    def _get_config(self, key):
1683 11
        env_value = self._get_env_config(key)
1684 11
        if env_value is not None:
1685 11
            return env_value
1686 11
        return self._get_profile_config(key)
1687

1688 11
    def _assume_role_with_web_identity(self):
1689 11
        token_path = self._get_config('web_identity_token_file')
1690 11
        if not token_path:
1691 11
            return None
1692 11
        token_loader = self._token_loader_cls(token_path)
1693

1694 11
        role_arn = self._get_config('role_arn')
1695 11
        if not role_arn:
1696 11
            error_msg = (
1697
                'The provided profile or the current environment is '
1698
                'configured to assume role with web identity but has no '
1699
                'role ARN configured. Ensure that the profile has the role_arn'
1700
                'configuration set or the AWS_ROLE_ARN env var is set.'
1701
            )
1702 11
            raise InvalidConfigError(error_msg=error_msg)
1703

1704 11
        extra_args = {}
1705 11
        role_session_name = self._get_config('role_session_name')
1706 11
        if role_session_name is not None:
1707 11
            extra_args['RoleSessionName'] = role_session_name
1708

1709 11
        fetcher = AssumeRoleWithWebIdentityCredentialFetcher(
1710
            client_creator=self._client_creator,
1711
            web_identity_token_loader=token_loader,
1712
            role_arn=role_arn,
1713
            extra_args=extra_args,
1714
            cache=self.cache,
1715
        )
1716
        # The initial credentials are empty and the expiration time is set
1717
        # to now so that we can delay the call to assume role until it is
1718
        # strictly needed.
1719 11
        return DeferredRefreshableCredentials(
1720
            method=self.METHOD,
1721
            refresh_using=fetcher.fetch_credentials,
1722
        )
1723

1724

1725 11
class CanonicalNameCredentialSourcer(object):
1726 11
    def __init__(self, providers):
1727 11
        self._providers = providers
1728

1729 11
    def is_supported(self, source_name):
1730
        """Validates a given source name.
1731

1732
        :type source_name: str
1733
        :param source_name: The value of credential_source in the config
1734
            file. This is the canonical name of the credential provider.
1735

1736
        :rtype: bool
1737
        :returns: True if the credential provider is supported,
1738
            False otherwise.
1739
        """
1740 11
        return source_name in [p.CANONICAL_NAME for p in self._providers]
1741

1742 11
    def source_credentials(self, source_name):
1743
        """Loads source credentials based on the provided configuration.
1744

1745
        :type source_name: str
1746
        :param source_name: The value of credential_source in the config
1747
            file. This is the canonical name of the credential provider.
1748

1749
        :rtype: Credentials
1750
        """
1751 11
        source = self._get_provider(source_name)
1752 11
        if isinstance(source, CredentialResolver):
1753 11
            return source.load_credentials()
1754 11
        return source.load()
1755

1756 11
    def _get_provider(self, canonical_name):
1757
        """Return a credential provider by its canonical name.
1758

1759
        :type canonical_name: str
1760
        :param canonical_name: The canonical name of the provider.
1761

1762
        :raises UnknownCredentialError: Raised if no
1763
            credential provider by the provided name
1764
            is found.
1765
        """
1766 11
        provider = self._get_provider_by_canonical_name(canonical_name)
1767

1768
        # The AssumeRole provider should really be part of the SharedConfig
1769
        # provider rather than being its own thing, but it is not. It is
1770
        # effectively part of both the SharedConfig provider and the
1771
        # SharedCredentials provider now due to the way it behaves.
1772
        # Therefore if we want either of those providers we should return
1773
        # the AssumeRole provider with it.
1774 11
        if canonical_name.lower() in ['sharedconfig', 'sharedcredentials']:
1775 11
            assume_role_provider = self._get_provider_by_method('assume-role')
1776 11
            if assume_role_provider is not None:
1777
                # The SharedConfig or SharedCredentials provider may not be
1778
                # present if it was removed for some reason, but the
1779
                # AssumeRole provider could still be present. In that case,
1780
                # return the assume role provider by itself.
1781 11
                if provider is None:
1782 11
                    return assume_role_provider
1783

1784
                # If both are present, return them both as a
1785
                # CredentialResolver so that calling code can treat them as
1786
                # a single entity.
1787 11
                return CredentialResolver([assume_role_provider, provider])
1788

1789 11
        if provider is None:
1790 11
            raise UnknownCredentialError(name=canonical_name)
1791

1792 11
        return provider
1793

1794 11
    def _get_provider_by_canonical_name(self, canonical_name):
1795
        """Return a credential provider by its canonical name.
1796

1797
        This function is strict, it does not attempt to address
1798
        compatibility issues.
1799
        """
1800 11
        for provider in self._providers:
1801 11
            name = provider.CANONICAL_NAME
1802
            # Canonical names are case-insensitive
1803 11
            if name and name.lower() == canonical_name.lower():
1804 11
                return provider
1805

1806 11
    def _get_provider_by_method(self, method):
1807
        """Return a credential provider by its METHOD name."""
1808 11
        for provider in self._providers:
1809 11
            if provider.METHOD == method:
1810 11
                return provider
1811

1812

1813 11
class ContainerProvider(CredentialProvider):
1814 11
    METHOD = 'container-role'
1815 11
    CANONICAL_NAME = 'EcsContainer'
1816 11
    ENV_VAR = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'
1817 11
    ENV_VAR_FULL = 'AWS_CONTAINER_CREDENTIALS_FULL_URI'
1818 11
    ENV_VAR_AUTH_TOKEN = 'AWS_CONTAINER_AUTHORIZATION_TOKEN'
1819

1820 11
    def __init__(self, environ=None, fetcher=None):
1821 11
        if environ is None:
1822 11
            environ = os.environ
1823 11
        if fetcher is None:
1824 11
            fetcher = ContainerMetadataFetcher()
1825 11
        self._environ = environ
1826 11
        self._fetcher = fetcher
1827

1828 11
    def load(self):
1829
        # This cred provider is only triggered if the self.ENV_VAR is set,
1830
        # which only happens if you opt into this feature.
1831 11
        if self.ENV_VAR in self._environ or self.ENV_VAR_FULL in self._environ:
1832 11
            return self._retrieve_or_fail()
1833

1834 11
    def _retrieve_or_fail(self):
1835 11
        if self._provided_relative_uri():
1836 11
            full_uri = self._fetcher.full_url(self._environ[self.ENV_VAR])
1837
        else:
1838 11
            full_uri = self._environ[self.ENV_VAR_FULL]
1839 11
        headers = self._build_headers()
1840 11
        fetcher = self._create_fetcher(full_uri, headers)
1841 11
        creds = fetcher()
1842 11
        return RefreshableCredentials(
1843
            access_key=creds['access_key'],
1844
            secret_key=creds['secret_key'],
1845
            token=creds['token'],
1846
            method=self.METHOD,
1847
            expiry_time=_parse_if_needed(creds['expiry_time']),
1848
            refresh_using=fetcher,
1849
        )
1850

1851 11
    def _build_headers(self):
1852 11
        headers = {}
1853 11
        auth_token = self._environ.get(self.ENV_VAR_AUTH_TOKEN)
1854 11
        if auth_token is not None:
1855 11
            return {
1856
                'Authorization': auth_token
1857
            }
1858

1859 11
    def _create_fetcher(self, full_uri, headers):
1860 11
        def fetch_creds():
1861 11
            try:
1862 11
                response = self._fetcher.retrieve_full_uri(
1863
                    full_uri, headers=headers)
1864 11
            except MetadataRetrievalError as e:
1865 11
                logger.debug("Error retrieving container metadata: %s", e,
1866
                             exc_info=True)
1867 11
                raise CredentialRetrievalError(provider=self.METHOD,
1868
                                               error_msg=str(e))
1869 11
            return {
1870
                'access_key': response['AccessKeyId'],
1871
                'secret_key': response['SecretAccessKey'],
1872
                'token': response['Token'],
1873
                'expiry_time': response['Expiration'],
1874
            }
1875

1876 11
        return fetch_creds
1877

1878 11
    def _provided_relative_uri(self):
1879 11
        return self.ENV_VAR in self._environ
1880

1881

1882 11
class CredentialResolver(object):
1883 11
    def __init__(self, providers):
1884
        """
1885

1886
        :param providers: A list of ``CredentialProvider`` instances.
1887

1888
        """
1889 11
        self.providers = providers
1890

1891 11
    def insert_before(self, name, credential_provider):
1892
        """
1893
        Inserts a new instance of ``CredentialProvider`` into the chain that
1894
        will be tried before an existing one.
1895

1896
        :param name: The short name of the credentials you'd like to insert the
1897
            new credentials before. (ex. ``env`` or ``config``). Existing names
1898
            & ordering can be discovered via ``self.available_methods``.
1899
        :type name: string
1900

1901
        :param cred_instance: An instance of the new ``Credentials`` object
1902
            you'd like to add to the chain.
1903
        :type cred_instance: A subclass of ``Credentials``
1904
        """
1905 11
        try:
1906 11
            offset = [p.METHOD for p in self.providers].index(name)
1907 0
        except ValueError:
1908 0
            raise UnknownCredentialError(name=name)
1909 11
        self.providers.insert(offset, credential_provider)
1910

1911 11
    def insert_after(self, name, credential_provider):
1912
        """
1913
        Inserts a new type of ``Credentials`` instance into the chain that will
1914
        be tried after an existing one.
1915

1916
        :param name: The short name of the credentials you'd like to insert the
1917
            new credentials after. (ex. ``env`` or ``config``). Existing names
1918
            & ordering can be discovered via ``self.available_methods``.
1919
        :type name: string
1920

1921
        :param cred_instance: An instance of the new ``Credentials`` object
1922
            you'd like to add to the chain.
1923
        :type cred_instance: A subclass of ``Credentials``
1924
        """
1925 11
        offset = self._get_provider_offset(name)
1926 11
        self.providers.insert(offset + 1, credential_provider)
1927

1928 11
    def remove(self, name):
1929
        """
1930
        Removes a given ``Credentials`` instance from the chain.
1931

1932
        :param name: The short name of the credentials instance to remove.
1933
        :type name: string
1934
        """
1935 11
        available_methods = [p.METHOD for p in self.providers]
1936 11
        if name not in available_methods:
1937
            # It's not present. Fail silently.
1938 11
            return
1939

1940 11
        offset = available_methods.index(name)
1941 11
        self.providers.pop(offset)
1942

1943 11
    def get_provider(self, name):
1944
        """Return a credential provider by name.
1945

1946
        :type name: str
1947
        :param name: The name of the provider.
1948

1949
        :raises UnknownCredentialError: Raised if no
1950
            credential provider by the provided name
1951
            is found.
1952
        """
1953 11
        return self.providers[self._get_provider_offset(name)]
1954

1955 11
    def _get_provider_offset(self, name):
1956 11
        try:
1957 11
            return [p.METHOD for p in self.providers].index(name)
1958 11
        except ValueError:
1959 11
            raise UnknownCredentialError(name=name)
1960

1961 11
    def load_credentials(self):
1962
        """
1963
        Goes through the credentials chain, returning the first ``Credentials``
1964
        that could be loaded.
1965
        """
1966
        # First provider to return a non-None response wins.
1967 11
        for provider in self.providers:
1968 11
            logger.debug("Looking for credentials via: %s", provider.METHOD)
1969 11
            creds = provider.load()
1970 11
            if creds is not None:
1971 11
                return creds
1972

1973
        # If we got here, no credentials could be found.
1974
        # This feels like it should be an exception, but historically, ``None``
1975
        # is returned.
1976
        #
1977
        # +1
1978
        # -js
1979 11
        return None
1980

1981

1982 11
class SSOCredentialFetcher(CachedCredentialFetcher):
1983 11
    def __init__(self, start_url, sso_region, role_name, account_id,
1984
                 client_creator, token_loader=None, cache=None,
1985
                 expiry_window_seconds=None):
1986 11
        self._client_creator = client_creator
1987 11
        self._sso_region = sso_region
1988 11
        self._role_name = role_name
1989 11
        self._account_id = account_id
1990 11
        self._start_url = start_url
1991 11
        self._token_loader = token_loader
1992

1993 11
        super(SSOCredentialFetcher, self).__init__(
1994
            cache, expiry_window_seconds
1995
        )
1996

1997 11
    def _create_cache_key(self):
1998
        """Create a predictable cache key for the current configuration.
1999

2000
        The cache key is intended to be compatible with file names.
2001
        """
2002 11
        args = {
2003
            'startUrl': self._start_url,
2004
            'roleName': self._role_name,
2005
            'accountId': self._account_id,
2006
        }
2007
        # NOTE: It would be good to hoist this cache key construction logic
2008
        # into the CachedCredentialFetcher class as we should be consistent.
2009
        # Unfortunately, the current assume role fetchers that sub class don't
2010
        # pass separators resulting in non-minified JSON. In the long term,
2011
        # all fetchers should use the below caching scheme.
2012 11
        args = json.dumps(args, sort_keys=True, separators=(',', ':'))
2013 11
        argument_hash = sha1(args.encode('utf-8')).hexdigest()
2014 11
        return self._make_file_safe(argument_hash)
2015

2016 11
    def _parse_timestamp(self, timestamp_ms):
2017
        # fromtimestamp expects seconds so: milliseconds / 1000 = seconds
2018 11
        timestamp_seconds = timestamp_ms / 1000.0
2019 11
        timestamp = datetime.datetime.fromtimestamp(timestamp_seconds, tzutc())
2020 11
        return _serialize_if_needed(timestamp)
2021

2022 11
    def _get_credentials(self):
2023
        """Get credentials by calling SSO get role credentials."""
2024 11
        config = Config(
2025
            signature_version=UNSIGNED,
2026
            region_name=self._sso_region,
2027
        )
2028 11
        client = self._client_creator('sso', config=config)
2029

2030 11
        kwargs = {
2031
            'roleName': self._role_name,
2032
            'accountId': self._account_id,
2033
            'accessToken': self._token_loader(self._start_url),
2034
        }
2035 11
        try:
2036 11
            response = client.get_role_credentials(**kwargs)
2037 11
        except client.exceptions.UnauthorizedException:
2038 11
            raise UnauthorizedSSOTokenError()
2039 11
        credentials = response['roleCredentials']
2040

2041 11
        credentials = {
2042
            'ProviderType': 'sso',
2043
            'Credentials': {
2044
                'AccessKeyId': credentials['accessKeyId'],
2045
                'SecretAccessKey': credentials['secretAccessKey'],
2046
                'SessionToken': credentials['sessionToken'],
2047
                'Expiration': self._parse_timestamp(credentials['expiration']),
2048
            }
2049
        }
2050 11
        return credentials
2051

2052

2053 11
class SSOProvider(CredentialProvider):
2054 11
    METHOD = 'sso'
2055

2056 11
    _SSO_TOKEN_CACHE_DIR = os.path.expanduser(
2057
        os.path.join('~', '.aws', 'sso', 'cache')
2058
    )
2059 11
    _SSO_CONFIG_VARS = [
2060
        'sso_start_url',
2061
        'sso_region',
2062
        'sso_role_name',
2063
        'sso_account_id',
2064
    ]
2065

2066 11
    def __init__(self, load_config, client_creator, profile_name,
2067
                 cache=None, token_cache=None):
2068 11
        if token_cache is None:
2069 11
            token_cache = JSONFileCache(self._SSO_TOKEN_CACHE_DIR)
2070 11
        self._token_cache = token_cache
2071 11
        if cache is None:
2072 11
            cache = {}
2073 11
        self.cache = cache
2074 11
        self._load_config = load_config
2075 11
        self._client_creator = client_creator
2076 11
        self._profile_name = profile_name
2077

2078 11
    def _load_sso_config(self):
2079 11
        loaded_config = self._load_config()
2080 11
        profiles = loaded_config.get('profiles', {})
2081 11
        profile_name = self._profile_name
2082 11
        profile_config = profiles.get(self._profile_name, {})
2083

2084 11
        if all(c not in profile_config for c in self._SSO_CONFIG_VARS):
2085 11
            return None
2086

2087 11
        config = {}
2088 11
        missing_config_vars = []
2089 11
        for config_var in self._SSO_CONFIG_VARS:
2090 11
            if config_var in profile_config:
2091 11
                config[config_var] = profile_config[config_var]
2092
            else:
2093 11
                missing_config_vars.append(config_var)
2094

2095 11
        if missing_config_vars:
2096 11
            missing = ', '.join(missing_config_vars)
2097 11
            raise InvalidConfigError(
2098
                error_msg=(
2099
                    'The profile "%s" is configured to use SSO but is missing '
2100
                    'required configuration: %s' % (profile_name, missing)
2101
                )
2102
            )
2103

2104 11
        return config
2105

2106 11
    def load(self):
2107 11
        sso_config = self._load_sso_config()
2108 11
        if not sso_config:
2109 11
            return None
2110

2111 11
        sso_fetcher = SSOCredentialFetcher(
2112
            sso_config['sso_start_url'],
2113
            sso_config['sso_region'],
2114
            sso_config['sso_role_name'],
2115
            sso_config['sso_account_id'],
2116
            self._client_creator,
2117
            token_loader=SSOTokenLoader(cache=self._token_cache),
2118
            cache=self.cache,
2119
        )
2120

2121 11
        return DeferredRefreshableCredentials(
2122
            method=self.METHOD,
2123
            refresh_using=sso_fetcher.fetch_credentials,
2124
        )

Read our documentation on viewing source code .

Loading