Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/feature-credentials-44796.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "credentials",
"description": "Add the ``credential_cache_location`` setting for assume role and assume role with web identity profiles. Set it to cache a profile's temporary credentials in a directory other than ``~/.aws/cli/cache``."
}
108 changes: 86 additions & 22 deletions awscli/botocore/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -1030,17 +1030,25 @@ def _assume_role_kwargs(self):
_CREDENTIAL_CACHE_FILE = 'file'
_CREDENTIAL_CACHE_MEMORY = 'memory'
_CREDENTIAL_CACHE_VALUES = (_CREDENTIAL_CACHE_FILE, _CREDENTIAL_CACHE_MEMORY)
# Used by ``credential_cache = file`` where no cache was injected. It's the
# directory the AWS CLI's injected role cache uses.
# Used by ``credential_cache = file`` with no location where no cache was
# injected. It's the directory the AWS CLI's injected role cache uses.
_DEFAULT_CREDENTIAL_CACHE_DIR = os.path.join('~', '.aws', 'cli', 'cache')


def _get_credential_cache_config(profile_name, profile):
"""Read and validate a role profile's ``credential_cache`` setting.

The setting controls whether a role profile's temporary credentials
are cached on disk or only in memory. Returns ``file``, ``memory`` or
``None`` when unset.
"""Read and validate a role profile's credential cache settings.

Reads the ``credential_cache`` and ``credential_cache_location``
profile keys, which control whether a role profile's temporary
credentials are cached on disk or only in memory. Returns a
``(credential_cache, credential_cache_location)`` tuple.

The location is ``None`` when the profile sets no
``credential_cache_location`` (the default ``~/.aws/cli/cache`` is used
as before, see ``_select_credential_cache``) and with ``memory``, which
ignores it. Otherwise it is the profile's ``credential_cache_location``
with ``~`` expanded. It's used like the default folder: the
``JSONFileCache`` creates it when it first writes.
"""
credential_cache = profile.get('credential_cache')
if (
Expand All @@ -1054,22 +1062,64 @@ def _get_credential_cache_config(profile_name, profile):
f'"{profile_name}" is not valid. Valid values are: {valid}.'
)
)
return credential_cache
if credential_cache == _CREDENTIAL_CACHE_MEMORY:
# memory never uses the location, so it isn't read or validated.
return credential_cache, None
location = profile.get('credential_cache_location')
if location is None:
# Unset or an explicit file with no location: the default
# ~/.aws/cli/cache is used as before.
return credential_cache, None
if not location.strip():
raise InvalidConfigError(
error_msg=(
f'The credential_cache_location in profile "{profile_name}" '
'is empty. Set it to a directory or remove it.'
)
)
expanded = os.path.expanduser(location)
if expanded.startswith('~'):
# expanduser leaves ~user unchanged when it can't find that user's
# home directory.
raise InvalidConfigError(
error_msg=(
f'The credential_cache_location "{location}" in profile '
f'"{profile_name}" could not be expanded: the home directory '
'for that user can\'t be found. Use an absolute path or a '
'path that starts with ~/.'
)
)
if not os.path.isabs(expanded):
message = (
f'The credential_cache_location "{location}" in profile '
f'"{profile_name}" is not an absolute path. Use an absolute '
'path or a path that starts with ~/.'
)
if '$' in location or '%' in location:
message += " Environment variables aren't expanded."
raise InvalidConfigError(error_msg=message)
return credential_cache, expanded


def _select_credential_cache(credential_cache, default_cache, memory_cache):
"""Pick the cache a role hop uses, from its profile's setting.
def _select_credential_cache(
credential_cache, credential_cache_location, default_cache, memory_cache
):
"""Pick the cache a role hop uses, from its profile's settings.

``memory`` uses ``memory_cache`` (a ``dict`` owned by the provider), so
nothing reaches the file cache. A profile that doesn't set the key, or
sets ``file``, uses the provider's injected cache (``default_cache``),
which is the behavior before this setting existed. Where nothing was
injected (``default_cache`` is the plain ``dict`` default, as on a web
identity source hop), an explicit ``file`` still caches on disk, in a
plain ``JSONFileCache`` in ``~/.aws/cli/cache``.
nothing reaches the file cache. A ``credential_cache_location`` uses a
plain ``JSONFileCache`` in that directory, like the default folder. A
profile that sets neither key, or ``file`` with no location, uses the
provider's injected cache (``default_cache``), which is the behavior
before these settings existed. Where nothing was injected
(``default_cache`` is the plain ``dict`` default, as on a web identity
source hop), an explicit ``file`` still caches on disk, in a plain
``JSONFileCache`` in ``~/.aws/cli/cache``.
"""
if credential_cache == _CREDENTIAL_CACHE_MEMORY:
return memory_cache
if credential_cache_location is not None:
return JSONFileCache(credential_cache_location)
if credential_cache == _CREDENTIAL_CACHE_FILE and (
default_cache is None or isinstance(default_cache, dict)
):
Expand Down Expand Up @@ -1724,6 +1774,7 @@ def _load_creds_via_assume_role(self, profile_name):
mfa_prompter=self._prompter,
cache=_select_credential_cache(
role_config['credential_cache'],
role_config['credential_cache_location'],
default_cache=self.cache,
memory_cache=self._memory_cache,
),
Expand Down Expand Up @@ -1756,7 +1807,9 @@ def _get_role_config(self, profile_name):
external_id = profile.get('external_id')
role_session_name = profile.get('role_session_name')
duration_seconds = profile.get('duration_seconds')
credential_cache = _get_credential_cache_config(profile_name, profile)
credential_cache, credential_cache_location = (
_get_credential_cache_config(profile_name, profile)
)

role_config = {
'role_arn': role_arn,
Expand All @@ -1766,6 +1819,7 @@ def _get_role_config(self, profile_name):
'source_profile': source_profile,
'credential_source': credential_source,
'credential_cache': credential_cache,
'credential_cache_location': credential_cache_location,
}

if duration_seconds is not None:
Expand Down Expand Up @@ -2004,15 +2058,25 @@ def _get_config(self, key):
return None

def _get_cache(self):
# credential_cache is a key of the profile in use, even when the
# The cache settings are keys of the profile in use, even when the
# token file or role ARN comes from the environment. A profile
# without it (or no profile) keeps the injected cache.
credential_cache = _get_credential_cache_config(
self._profile_name,
{'credential_cache': self._get_profile_config('credential_cache')},
# without them (or no profile) keeps the injected cache.
credential_cache, credential_cache_location = (
_get_credential_cache_config(
self._profile_name,
{
'credential_cache': self._get_profile_config(
'credential_cache'
),
'credential_cache_location': self._get_profile_config(
'credential_cache_location'
),
},
)
)
return _select_credential_cache(
credential_cache,
credential_cache_location,
default_cache=self.cache,
memory_cache=self._memory_cache,
)
Expand Down
66 changes: 45 additions & 21 deletions awscli/topics/config-vars.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,27 +286,43 @@ in the AWS CLI config file:
* ``credential_cache`` - Where the AWS CLI caches the temporary credentials
from the ``AssumeRole`` call for this profile. Valid values are ``file``
(the default) and ``memory``. With ``file``, the credentials are stored on
disk, in ``~/.aws/cli/cache``, and reused by later commands until they
expire. With ``memory``, they are kept only for the duration of a single
AWS CLI command and are never written to disk. This is an optional
disk, in ``credential_cache_location``, and reused by later commands until
they expire. With ``memory``, they are kept only for the duration of a
single AWS CLI command and are never written to disk. This is an optional
parameter.
* ``credential_cache_location`` - The directory where the AWS CLI stores the
cached temporary credentials for this profile when ``credential_cache`` is
``file``. The value must be an absolute path or a path that starts with
``~/``; a relative path is an error, and environment variables are not
expanded. With ``credential_cache = memory``, this setting is ignored. This
is an optional parameter, and by default the value is ``~/.aws/cli/cache``.

The AWS CLI uses a ``credential_cache_location`` directory the same way as
the default ``~/.aws/cli/cache``. If the directory does not exist, it is
created. Cache files are written the same way as in the default cache
directory, readable only by you (mode ``600``). Choosing a directory that
other users can't access is up to you.

Switching a profile to ``credential_cache = memory`` does not delete the
credentials it has already cached. To remove them, delete the files in
``~/.aws/cli/cache``; the AWS CLI recreates the ones it needs.
credentials it has already cached. To remove them, delete the files in the
cache directory (``~/.aws/cli/cache``, or the profile's
``credential_cache_location``); the AWS CLI recreates the ones it needs.

Only the AWS CLI reads ``credential_cache``. The AWS SDKs and other tools
that read the shared config file ignore it, so a tool that keeps its own file
cache may still write credentials to disk for a profile set to ``memory``.
Only the AWS CLI reads ``credential_cache`` and
``credential_cache_location``. The AWS SDKs and other tools that read the
shared config file ignore them, so a tool that keeps its own file cache may
still write credentials to disk for a profile set to ``memory``.

In a chain of roles, where a ``source_profile`` is itself a profile that
assumes a role, each profile's ``credential_cache`` applies only to that
profile's ``AssumeRole`` call.
assumes a role, each profile's ``credential_cache`` and
``credential_cache_location`` apply only to that profile's ``AssumeRole``
call.

When the ``source_profile`` is an assume role with web identity profile that
doesn't set ``credential_cache``, its credentials are cached only in memory,
as in earlier versions of the AWS CLI. To cache that hop on disk in
``~/.aws/cli/cache``, set ``credential_cache = file`` in that profile.
sets neither setting, its credentials are cached only in memory, as in earlier
versions of the AWS CLI. To cache that hop on disk, set
``credential_cache = file`` (which uses ``~/.aws/cli/cache``) or
``credential_cache_location`` in that profile.

.. warning::

Expand All @@ -324,7 +340,7 @@ If you do not have MFA authentication required, then you only need to specify a
When you specify a profile that has IAM role configuration, the AWS CLI
will make an ``AssumeRole`` call to retrieve temporary credentials. These
credentials are then stored (in ``~/.aws/cli/cache``, unless the profile sets
``credential_cache = memory``). Subsequent AWS CLI
``credential_cache`` or ``credential_cache_location``). Subsequent AWS CLI
commands will use the cached temporary credentials until they expire, in which
case the AWS CLI will automatically refresh credentials.

Expand Down Expand Up @@ -388,10 +404,18 @@ assume role with web identity profile in the shared config:
from the ``AssumeRoleWithWebIdentity`` call: ``file`` (the default) or
``memory``. See ``credential_cache`` under `Using AWS IAM Roles`_, including
the warning about AWS STS quotas.

Only the AWS CLI reads ``credential_cache``. The AWS SDKs and other tools
that read the shared config file ignore it, so a tool that keeps its own file
cache may still write credentials to disk for a profile set to ``memory``.
* ``credential_cache_location`` - The directory where the AWS CLI stores the
cached temporary credentials when ``credential_cache`` is ``file``. The
value must be an absolute path or a path that starts with ``~``; a relative
path is an error. The directory must be private to you, and is checked in
the same way as under `Using AWS IAM Roles`_. With
``credential_cache = memory``, this setting is ignored. By default, the
value is ``~/.aws/cli/cache``.

Only the AWS CLI reads ``credential_cache`` and
``credential_cache_location``. The AWS SDKs and other tools that read the
shared config file ignore them, so a tool that keeps its own file cache may
still write credentials to disk for a profile set to ``memory``.

Below is an example configuration for the minimal amount of configuration
needed to configure an assume role with web identity profile::
Expand All @@ -417,9 +441,9 @@ This provider can also be configured via the environment:
These environment variables currently only apply to the assume role with
web identity provider and do not apply to the general assume role provider
configuration. When the provider is configured through these environment
variables, the ``credential_cache`` setting of the profile in use still
applies. If the profile does not set it, credentials are cached in
``~/.aws/cli/cache``.
variables, the ``credential_cache`` and ``credential_cache_location``
settings of the profile in use still apply. If the profile does not set
them, credentials are cached in ``~/.aws/cli/cache``.


Sourcing Credentials From External Processes
Expand Down
46 changes: 45 additions & 1 deletion tests/functional/botocore/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,15 @@ def test_default_reuses_cached_credentials(self):
self.assertEqual(first, second)
self.assertEqual(len(self.cache_files()), 1)

def test_custom_location_reuses_cached_credentials(self):
location = os.path.join(self.tempdir, 'role-cache')
self.write_role_config(f'credential_cache_location = {location}\n')
first = self.run_command('A', 'assume_role', responses=1)
second = self.run_command('A', 'assume_role', responses=0)
self.assertEqual(first, second)
self.assertEqual(self.cache_files(), [])
self.assertEqual(len(os.listdir(location)), 1)

def test_invalid_value_raises(self):
self.write_role_config('credential_cache = off\n')
session = StubbedSession(profile='A')
Expand Down Expand Up @@ -1021,6 +1030,21 @@ def test_env_only_web_identity_uses_default_cache(self):
self.run_command(None, operation, responses=0)
self.assertEqual(len(self.cache_files()), 1)

def test_relative_location_raises(self):
self.write_role_config('credential_cache_location = relcache\n')
session = StubbedSession(profile='A')
with self.assertRaisesRegex(
InvalidConfigError, 'credential_cache_location'
):
session.get_credentials()

def test_memory_ignores_empty_location(self):
self.write_role_config(
'credential_cache = memory\ncredential_cache_location =\n'
)
self.run_command('A', 'assume_role', responses=1)
self.assertEqual(self.cache_files(), [])

def write_web_identity_source_hop_config(self, extra=''):
self.write_config(
'[profile A]\n'
Expand Down Expand Up @@ -1060,14 +1084,34 @@ def test_web_identity_source_hop_explicit_file_uses_default_dir(self):
self.assertEqual(len(self.cache_files()), 2)

def test_explicit_file_reuses_injected_cache(self):
# An explicit file behaves as unset: the injected
# Explicit file with no location behaves as unset: the injected
# ~/.aws/cli/cache is used.
self.write_role_config('credential_cache = file\n')
first = self.run_command('A', 'assume_role', responses=1)
second = self.run_command('A', 'assume_role', responses=0)
self.assertEqual(first, second)
self.assertEqual(len(self.cache_files()), 1)

def test_web_identity_source_hop_location_writes_there(self):
# A location on the hop applies, as on a top-level profile.
location = os.path.join(self.tempdir, 'hop-cache')
self.write_web_identity_source_hop_config(
f'credential_cache_location = {location}\n'
)
self.run_command_with_calls('A', self.source_hop_calls())
self.assertEqual(len(self.cache_files()), 1)
self.assertEqual(len(os.listdir(location)), 1)

def test_web_identity_source_hop_relative_location_raises(self):
self.write_web_identity_source_hop_config(
'credential_cache_location = relcache\n'
)
session = StubbedSession(profile='A')
with self.assertRaisesRegex(
InvalidConfigError, 'credential_cache_location'
):
session.get_credentials().get_frozen_credentials()


class TestProcessProvider(unittest.TestCase):
def setUp(self):
Expand Down
Loading
Loading