Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Commit. #656

Closed
wants to merge 4 commits into from
Closed
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
29 changes: 2 additions & 27 deletions awscli/customizations/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@
from botocore.exceptions import ProfileNotFound

from awscli.customizations.commands import BasicCommand


try:
raw_input = raw_input
except NameError:
raw_input = input
from awscli.customizations.utils import mask_value, InteractivePrompter


logger = logging.getLogger(__name__)
Expand All @@ -44,33 +39,13 @@ def __init__(self, value, config_type, config_variable):
def mask_value(self):
if self.value is NOT_SET:
return
self.value = _mask_value(self.value)
self.value = mask_value(self.value)


class SectionNotFoundError(Exception):
pass


def _mask_value(current_value):
if current_value is None:
return 'None'
else:
return ('*' * 16) + current_value[-4:]


class InteractivePrompter(object):
def get_value(self, current_value, config_name, prompt_text=''):
if config_name in ('aws_access_key_id', 'aws_secret_access_key'):
current_value = _mask_value(current_value)
response = raw_input("%s [%s]: " % (prompt_text, current_value))
if not response:
# If the user hits enter, we return a value of None
# instead of an empty string. That way we can determine
# whether or not a value has changed.
response = None
return response


class ConfigFileWriter(object):
SECTION_REGEX = re.compile(r'\[(?P<header>[^]]+)\]')
OPTION_REGEX = re.compile(
Expand Down
187 changes: 187 additions & 0 deletions awscli/customizations/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Copyright 2013 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 logging
import webbrowser

from botocore.compat import json, urlencode
import botocore.vendored.requests

from awscli.customizations.commands import BasicCommand
from awscli.customizations.commands import BasicDocHandler
from awscli.customizations.utils import InteractivePrompter


LOG = logging.getLogger(__name__)

issuer_url = 'https://mysignin.internal.mycompany.com/'
console_url = 'https://console.aws.amazon.com/'
signin_url = 'https://signin.aws.amazon.com/federation'


def register_session_cmd(cli):
cli.register('building-command-table.main',
SessionCommand.add_command)


class NoTemporaryCredentialsError(Exception):
pass


class SessionDocHandler(BasicDocHandler):

def doc_subitems_start(self, help_command, **kwargs):
self.doc.style.h2('Available Commands')

def doc_subitem(self, command_name, help_command, **kwargs):
doc = help_command.doc
doc.style.tocitem(command_name)

def doc_subitems_end(self, help_command, **kwargs):
pass


class StartCommand(BasicCommand):

NAME = 'start'
DESCRIPTION = 'Start a new session.'
SYNOPSIS = ('aws session start [mfa-serial-number SN]'
'[--assume-role role-arn]')
EXAMPLES = (
'To start a new session::\n'
'\n'
' $aws session start\n'
'\n'
'To start a new MFA session::\n'
'\n'
' $ aws session start --mfa-serial-number\n'
' MFA Token [None]: token\n'
'\n'
'You can store your MFA serial number in your config file '
'using the name mfa_serial_number.\n\n'
'To start a session and assume an IAM Role::\n'
'\n'
' $ aws session start --assume-role role_arn\n'
)
ARG_TABLE = [
{'name': 'mfa-serial-number',
'help_text': 'The serial number of your MFA device, if needed.',
'action': 'store', 'cli_type_name': 'String'},
{'name': 'role-arn', 'help_text': 'Assume an IAM Role',
'action': 'store', 'required': False, 'cli_type_name': 'string'}]

def __init__(self, session, prompter=None):
super(StartCommand, self).__init__(session)
if prompter is None:
prompter = InteractivePrompter()
self._prompter = prompter

def _run_main(self, parsed_args, parsed_globals):
if parsed_args.role_arn is not None:
self._session.create_temporary_credentials(
'sts', 'AssumeRole', role_arn=parsed_args.role_arn,
role_session_name='awscli')
else:
serial_number = parsed_args.mfa_serial_number
token = None
if serial_number is None:
serial_number = self._session.get_variable('mfa_serial_number')
if serial_number:
token = self._prompter.get_value('', 'mfa_token', 'MFA Token')
self._session.create_temporary_credentials(
'sts', 'GetSessionToken', serial_number=serial_number,
token_code=token)


class EndCommand(BasicCommand):

NAME = 'end'
DESCRIPTION = 'End the current session.'
SYNOPSIS = 'aws session end'
EXAMPLES = (
'\n'
'To clear temporary credentials and use regular credentials::\n'
'\n'
' $ aws session end\n'
)

def _run_main(self, parsed_args, parsed_globals):
self._session.delete_temporary_credentials()


class ShowCommand(BasicCommand):

NAME = 'show'
DESCRIPTION = 'Show the current session, if any.'
SYNOPSIS = 'aws session show'
EXAMPLES = (
'\n'
'To display information about the current session, if any::\n'
'\n'
' $ aws session show\n'
)

def _run_main(self, parsed_args, parsed_globals):
creds = self._session.get_credentials()
if creds.method == 'session-cache':
print('Your current session:')
print('\tCredential Operation: %s' % creds.credential_operation)
for param in creds.credential_params:
print('\t%s: %s' % (param, creds.credential_params[param]))
else:
print('No current session')


class ConsoleCommand(BasicCommand):

NAME = 'console'
DESCRIPTION = 'Launch the web console using temporary credentials.'
SYNOPSIS = 'aws session console'
EXAMPLES = (
'\n'
'To open the AWS web console in a new tab of your default '
'browser:\n'
' $ aws session console\n'
)

def _run_main(self, parsed_args, parsed_globals):
creds = self._session.get_credentials()
if not creds.token:
msg = ('The console can only be launched with '
'temporary credentials.')
raise NoTemporaryCredentialsError(msg)
session_data = {'sessionId': creds.access_key,
'sessionKey': creds.secret_key,
'sessionToken': creds.token}
session = json.dumps(session_data)
params = {'Action': 'getSigninToken',
'Session': session}
r = botocore.vendored.requests.get(signin_url, params=params)
params = json.loads(r.text)
params['Action'] = 'login'
params['Issuer'] = issuer_url
params['Destination'] = console_url
url = signin_url + '?' + urlencode(params)
webbrowser.open(url)


class SessionCommand(BasicCommand):
NAME = 'session'
DESCRIPTION = 'The description'
SYNOPSIS = 'aws session start|end|help'
EXAMPLES = 'put examples here'
SUBCOMMANDS = [
{'name': 'start', 'command_class': StartCommand},
{'name': 'end', 'command_class': EndCommand},
{'name': 'console', 'command_class': ConsoleCommand},
{'name': 'show', 'command_class': ShowCommand}
]
27 changes: 27 additions & 0 deletions awscli/customizations/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
Utility functions to make it easier to work with customizations.

"""

try:
raw_input = raw_input
except NameError:
raw_input = input


def rename_argument(argument_table, existing_name, new_name):
current = argument_table[existing_name]
argument_table[new_name] = current
Expand Down Expand Up @@ -60,3 +67,23 @@ def _get_group_for_key(key, groups):
for group in groups:
if key in group:
return group


def mask_value(current_value):
if current_value is None:
return 'None'
else:
return ('*' * 16) + current_value[-4:]


class InteractivePrompter(object):
def get_value(self, current_value, config_name, prompt_text=''):
if config_name in ('aws_access_key_id', 'aws_secret_access_key'):
current_value = mask_value(current_value)
response = raw_input("%s [%s]: " % (prompt_text, current_value))
if not response:
# If the user hits enter, we return a value of None
# instead of an empty string. That way we can determine
# whether or not a value has changed.
response = None
return response
2 changes: 2 additions & 0 deletions awscli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from awscli.customizations.cloudtrail import initialize as cloudtrail_init
from awscli.customizations.toplevelbool import register_bool_params
from awscli.customizations.ec2protocolarg import register_protocol_args
from awscli.customizations.session import register_session_cmd


def awscli_initialize(event_handlers):
Expand Down Expand Up @@ -85,3 +86,4 @@ def awscli_initialize(event_handlers):
cloudtrail_init(event_handlers)
register_bool_params(event_handlers)
register_protocol_args(event_handlers)
register_session_cmd(event_handlers)
9 changes: 5 additions & 4 deletions tests/unit/customizations/test_configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from six import StringIO
from botocore.exceptions import ProfileNotFound

import awscli.customizations.utils
from awscli.customizations import configure


Expand Down Expand Up @@ -179,7 +180,7 @@ def test_session_says_profile_does_not_exist(self):


class TestInteractivePrompter(unittest.TestCase):
@mock.patch('awscli.customizations.configure.raw_input')
@mock.patch('awscli.customizations.utils.raw_input')
def test_access_key_is_masked(self, mock_raw_input):
mock_raw_input.return_value = 'foo'
prompter = configure.InteractivePrompter()
Expand All @@ -193,7 +194,7 @@ def test_access_key_is_masked(self, mock_raw_input):
self.assertNotIn('myaccesskey', prompt_text)
self.assertRegexpMatches(prompt_text, r'\[\*\*\*\*.*\]')

@mock.patch('awscli.customizations.configure.raw_input')
@mock.patch('awscli.customizations.utils.raw_input')
def test_access_key_not_masked_when_none(self, mock_raw_input):
mock_raw_input.return_value = 'foo'
prompter = configure.InteractivePrompter()
Expand All @@ -205,7 +206,7 @@ def test_access_key_not_masked_when_none(self, mock_raw_input):
prompt_text = mock_raw_input.call_args[0][0]
self.assertIn('[None]', prompt_text)

@mock.patch('awscli.customizations.configure.raw_input')
@mock.patch('awscli.customizations.utils.raw_input')
def test_secret_key_is_masked(self, mock_raw_input):
prompter = configure.InteractivePrompter()
prompter.get_value(
Expand All @@ -217,7 +218,7 @@ def test_secret_key_is_masked(self, mock_raw_input):
self.assertNotIn('mysupersecretkey', prompt_text)
self.assertRegexpMatches(prompt_text, r'\[\*\*\*\*.*\]')

@mock.patch('awscli.customizations.configure.raw_input')
@mock.patch('awscli.customizations.utils.raw_input')
def test_non_secret_keys_are_not_masked(self, mock_raw_input):
prompter = configure.InteractivePrompter()
prompter.get_value(
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
'elasticache', 'elasticbeanstalk', 'elastictranscoder',
'elb', 'emr', 'iam', 'importexport', 'kinesis',
'opsworks', 'rds', 'redshift', 'route53', 's3', 's3api',
'ses', 'sns', 'sqs', 'storagegateway', 'sts', 'support',
'swf'])),
'ses', 'session', 'sns', 'sqs', 'storagegateway',
'sts', 'support', 'swf'])),
('aws cloud', -1, set(['cloudformation', 'cloudfront',
'cloudsearch', 'cloudtrail', 'cloudwatch'])),
('aws cloudf', -1, set(['cloudformation', 'cloudfront'])),
Expand Down