forked from aws/aws-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
3 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.