Skip to content

Commit

Permalink
Add export-creds command to the CLI
Browse files Browse the repository at this point in the history
This PR builds on the interface proposed in aws#6808 and implements
the additional features proposed in aws#7388.

From the original PRs, the additional features are:

* Added support for an explicit `--format` args to control the output
  format.
* Add support for env vars, powershell/windows vars, and a JSON format
  that's enables this command to be used as a `credential_process`.
* Detect, and prevent infinite recursion when the credential process
  resolution results in the CLI calling itself with the same command.

Closes aws#7388
Closes aws#5261
  • Loading branch information
jamesls committed Nov 2, 2022
1 parent ecbb197 commit ace597d
Show file tree
Hide file tree
Showing 3 changed files with 396 additions and 0 deletions.
3 changes: 3 additions & 0 deletions awscli/customizations/configure/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from awscli.customizations.configure.importer import ConfigureImportCommand
from awscli.customizations.configure.listprofiles import ListProfilesCommand
from awscli.customizations.configure.sso import ConfigureSSOCommand
from awscli.customizations.configure.exportcreds import \
ConfigureExportCredsCommand

from . import mask_value, profile_to_section

Expand Down Expand Up @@ -80,6 +82,7 @@ class ConfigureCommand(BasicCommand):
{'name': 'import', 'command_class': ConfigureImportCommand},
{'name': 'list-profiles', 'command_class': ListProfilesCommand},
{'name': 'sso', 'command_class': ConfigureSSOCommand},
{'name': 'export-creds', 'command_class': ConfigureExportCredsCommand},
]

# If you want to add new values to prompt, update this list here.
Expand Down
183 changes: 183 additions & 0 deletions awscli/customizations/configure/exportcreds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import os
import sys
import json
from datetime import datetime
from collections import namedtuple

from awscli.customizations.commands import BasicCommand


# Takes botocore's ReadOnlyCredentials and exposes an expiry_time.
Credentials = namedtuple(
'Credentials', ['access_key', 'secret_key', 'token', 'expiry_time'])


def convert_botocore_credentials(credentials):
# Converts botocore credentials to our `Credentials` type.
frozen = credentials.get_frozen_credentials()
expiry_time_str = None
# Botocore does not expose an attribute for the expiry_time of temporary
# credentials, so for the time being we need to access an internal
# attribute to retrieve this info. We're following up to see if botocore
# can make this a public attribute.
expiry_time = getattr(credentials, '_expiry_time', None)
if expiry_time is not None and isinstance(expiry_time, datetime):
expiry_time_str = expiry_time.isoformat()
return Credentials(
access_key=frozen.access_key,
secret_key=frozen.secret_key,
token=frozen.token,
expiry_time=expiry_time_str,
)


class BaseCredentialFormatter(object):

FORMAT = None

def __init__(self, stream=None):
if stream is None:
stream = sys.stdout
self._stream = stream

def display_credentials(self, credentials):
pass


class BashEnvVarFormatter(BaseCredentialFormatter):

FORMAT = 'env'

def display_credentials(self, credentials):
output = (
f'export AWS_ACCESS_KEY_ID={credentials.access_key}\n'
f'export AWS_SECRET_ACCESS_KEY={credentials.secret_key}\n'
)
if credentials.token is not None:
output += f'export AWS_SESSION_TOKEN={credentials.token}\n'
self._stream.write(output)


class PowershellFormatter(BaseCredentialFormatter):

FORMAT = 'powershell'

def display_credentials(self, credentials):
output = (
f'$Env:AWS_ACCESS_KEY_ID="{credentials.access_key}"\n'
f'$Env:AWS_SECRET_ACCESS_KEY="{credentials.secret_key}"\n'
)
if credentials.token is not None:
output += f'$Env:AWS_SESSION_TOKEN="{credentials.token}"\n'
self._stream.write(output)


class WindowsCmdFormatter(BaseCredentialFormatter):

FORMAT = 'windows-cmd'

def display_credentials(self, credentials):
output = (
f'set AWS_ACCESS_KEY_ID={credentials.access_key}\n'
f'set AWS_SECRET_ACCESS_KEY={credentials.secret_key}\n'
)
if credentials.token is not None:
output += f'set AWS_SESSION_TOKEN={credentials.token}\n'
self._stream.write(output)


class CredentialProcessFormatter(BaseCredentialFormatter):

FORMAT = 'process'

def display_credentials(self, credentials):
output = {
'Version': 1,
'AccessKeyId': credentials.access_key,
'SecretAccessKey': credentials.secret_key,
}
if credentials.token is not None:
output['SessionToken'] = credentials.token
if credentials.expiry_time is not None:
output['Expiration'] = credentials.expiry_time
self._stream.write(
json.dumps(output, indent=2, separators=(',', ': '))
)
self._stream.write('\n')


SUPPORTED_FORMATS = {
format_cls.FORMAT: format_cls for format_cls in
[BashEnvVarFormatter, CredentialProcessFormatter, PowershellFormatter,
WindowsCmdFormatter]
}


class ConfigureExportCredsCommand(BasicCommand):
NAME = 'export-creds'
SYNOPSIS = 'aws configure export-creds --profile profile-name'
ARG_TABLE = [
{'name': 'format',
'help_text': (
'The output format to display credentials.'
'Defaults to "process".'),
'action': 'store',
'choices': list(SUPPORTED_FORMATS),
'default': CredentialProcessFormatter.FORMAT},
]
_RECURSION_VAR = '_AWS_CLI_RESOLVING_CREDS'

def __init__(self, session, out_stream=None, error_stream=None, env=None):
super(ConfigureExportCredsCommand, self).__init__(session)
if out_stream is None:
out_stream = sys.stdout
if error_stream is None:
error_stream = sys.stderr
if env is None:
env = os.environ
self._out_stream = out_stream
self._error_stream = error_stream
self._env = env

def _recursion_detected(self):
return self._RECURSION_VAR in self._env

def _set_recursion_barrier(self):
self._env[self._RECURSION_VAR] = 'true'

def _run_main(self, parsed_args, parsed_globals):
if self._recursion_detected():
self._error_stream.write(
"\n\nRecursive credential resolution process detected.\n"
"Try setting an explicit '--profile' value in the "
"'credential_process' configuration:\n\n"
"credential_process = aws configure export-creds "
"--profile other-profile\n"
)
return 2
self._set_recursion_barrier()
try:
creds = self._session.get_credentials()
except Exception as e:
self._error_stream.write(
"Unable to retrieve credentials: %s\n" % e)
return 1
if creds is None:
self._error_stream.write(
"Unable to retrieve credentials: no credentials found\n")
return 1
creds_with_expiry = convert_botocore_credentials(creds)
formatter = SUPPORTED_FORMATS[parsed_args.format](self._out_stream)
formatter.display_credentials(creds_with_expiry)

0 comments on commit ace597d

Please sign in to comment.