Skip to content

Commit

Permalink
Merge pull request #86 from endorama/get-pass-from-stdin
Browse files Browse the repository at this point in the history
Enable getting password from stdin
  • Loading branch information
stevemac007 authored Aug 7, 2018
2 parents a3450e0 + 38bac15 commit c265409
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 40 deletions.
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ be able to use this via Docker; the Docker container will not be able to
access any devices connected to the host ports. You will likely see the
following error during runtime: "RuntimeWarning: U2F Device Not Found".

Feeding password from stdin
~~~~~~~~~~~~~~~~~~~~~~~~~~~

To enhance usability when using third party tools for managing passwords (aka password manager) you can feed data in
``aws-google-auth`` from ``stdin``.

When receiving data from ``stdin`` ``aws-google-auth`` disables the interactive prompt and uses ``stdin`` data.

Before `#82 <https://github.com/cevoaustralia/aws-google-auth/issues/82>`_, all interactive prompts could be fed from ``stdin`` already apart from the ``Google Password:`` prompt.

Example usage:
::
$ password-manager show password | aws-google-auth
Google Password: MFA token:
Assuming arn:aws:iam::123456789012:role/admin
Credentials Expiration: ...

**Note:** this feature is intended for password manager integration, not for passing passwords from command line.
Please use interactive prompt if you need to pass the password manually, as this provide enhanced security avoid
password leakage to shell history.

Storage of profile credentials
------------------------------
Expand Down
5 changes: 2 additions & 3 deletions aws_google_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from . import amazon

import argparse
import getpass
import keyring
import os
import sys
Expand Down Expand Up @@ -175,9 +174,9 @@ def process_auth(args, config):
if keyring_password:
config.password = keyring_password
else:
config.password = getpass.getpass("Google Password: ")
config.password = util.Util.get_password("Google Password: ")
else:
config.password = getpass.getpass("Google Password: ")
config.password = util.Util.get_password("Google Password: ")

# Validate Options
config.raise_if_invalid()
Expand Down
52 changes: 15 additions & 37 deletions aws_google_auth/tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,10 @@ def test_main_method_chaining(self, process_auth, resolve_config, exit_if_unsupp
],
process_auth.mock_calls)

@patch('getpass.getpass', spec=True)
@patch('aws_google_auth.util', spec=True)
@patch('aws_google_auth.amazon', spec=True)
@patch('aws_google_auth.google', spec=True)
def test_process_auth_standard(self, mock_google, mock_amazon, mock_util, mock_getpass):
def test_process_auth_standard(self, mock_google, mock_amazon, mock_util):

mock_config = Mock()
mock_config.profile = False
Expand All @@ -93,8 +92,6 @@ def test_process_auth_standard(self, mock_google, mock_amazon, mock_util, mock_g
mock_amazon_client = Mock()
mock_google_client = Mock()

mock_getpass.return_value = "pass"

mock_amazon_client.roles = {
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps',
'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'
Expand All @@ -103,6 +100,7 @@ def test_process_auth_standard(self, mock_google, mock_amazon, mock_util, mock_g
mock_util_obj = MagicMock()
mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider"))
mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"])
mock_util_obj.get_password = MagicMock(return_value="pass")

mock_util.Util = mock_util_obj

Expand All @@ -129,16 +127,14 @@ def test_process_auth_standard(self, mock_google, mock_amazon, mock_util, mock_g
self.assertEqual([call.Util.get_input('Google username: '),
call.Util.get_input('Google IDP ID: '),
call.Util.get_input('Google SP ID: '),
call.Util.get_password('Google Password: '),
call.Util.pick_a_role({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps',
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])],
mock_util.mock_calls)

self.assertEqual([call()],
mock_amazon_client.print_export_line.mock_calls)

self.assertEqual([call('Google Password: ')],
mock_getpass.mock_calls)

self.assertEqual([call.do_login(), call.parse_saml()],
mock_google_client.mock_calls)

Expand All @@ -154,11 +150,10 @@ def test_process_auth_standard(self, mock_google, mock_amazon, mock_util, mock_g
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])
], mock_util_obj.pick_a_role.mock_calls)

@patch('getpass.getpass', spec=True)
@patch('aws_google_auth.util', spec=True)
@patch('aws_google_auth.amazon', spec=True)
@patch('aws_google_auth.google', spec=True)
def test_process_auth_specified_role(self, mock_google, mock_amazon, mock_util, mock_getpass):
def test_process_auth_specified_role(self, mock_google, mock_amazon, mock_util):

mock_config = Mock()
mock_config.saml_cache = False
Expand All @@ -174,8 +169,6 @@ def test_process_auth_specified_role(self, mock_google, mock_amazon, mock_util,
mock_amazon_client = Mock()
mock_google_client = Mock()

mock_getpass.return_value = "pass"

mock_amazon_client.roles = {
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps',
'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'
Expand All @@ -184,6 +177,7 @@ def test_process_auth_specified_role(self, mock_google, mock_amazon, mock_util,
mock_util_obj = MagicMock()
mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider"))
mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"])
mock_util_obj.get_password = MagicMock(return_value="pass")

mock_util.Util = mock_util_obj

Expand All @@ -208,12 +202,10 @@ def test_process_auth_specified_role(self, mock_google, mock_amazon, mock_util,
# Assert calls occur
self.assertEqual([call.Util.get_input('Google username: '),
call.Util.get_input('Google IDP ID: '),
call.Util.get_input('Google SP ID: ')],
call.Util.get_input('Google SP ID: '),
call.Util.get_password('Google Password: ')],
mock_util.mock_calls)

self.assertEqual([call('Google Password: ')],
mock_getpass.mock_calls)

self.assertEqual([call.do_login(), call.parse_saml()],
mock_google_client.mock_calls)

Expand All @@ -227,11 +219,10 @@ def test_process_auth_specified_role(self, mock_google, mock_amazon, mock_util,
self.assertEqual([],
mock_util_obj.pick_a_role.mock_calls)

@patch('getpass.getpass', spec=True)
@patch('aws_google_auth.util', spec=True)
@patch('aws_google_auth.amazon', spec=True)
@patch('aws_google_auth.google', spec=True)
def test_process_auth_dont_resolve_alias(self, mock_google, mock_amazon, mock_util, mock_getpass):
def test_process_auth_dont_resolve_alias(self, mock_google, mock_amazon, mock_util):

mock_config = Mock()
mock_config.saml_cache = False
Expand All @@ -245,8 +236,6 @@ def test_process_auth_dont_resolve_alias(self, mock_google, mock_amazon, mock_ut
mock_amazon_client = Mock()
mock_google_client = Mock()

mock_getpass.return_value = "pass"

mock_amazon_client.roles = {
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps',
'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'
Expand All @@ -255,6 +244,7 @@ def test_process_auth_dont_resolve_alias(self, mock_google, mock_amazon, mock_ut
mock_util_obj = MagicMock()
mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider"))
mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"])
mock_util_obj.get_password = MagicMock(return_value="pass")

mock_util.Util = mock_util_obj

Expand All @@ -280,13 +270,11 @@ def test_process_auth_dont_resolve_alias(self, mock_google, mock_amazon, mock_ut
self.assertEqual([call.Util.get_input('Google username: '),
call.Util.get_input('Google IDP ID: '),
call.Util.get_input('Google SP ID: '),
call.Util.get_password('Google Password: '),
call.Util.pick_a_role({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps',
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'})],
mock_util.mock_calls)

self.assertEqual([call('Google Password: ')],
mock_getpass.mock_calls)

self.assertEqual([call.do_login(), call.parse_saml()],
mock_google_client.mock_calls)

Expand All @@ -301,11 +289,10 @@ def test_process_auth_dont_resolve_alias(self, mock_google, mock_amazon, mock_ut
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'})
], mock_util_obj.pick_a_role.mock_calls)

@patch('getpass.getpass', spec=True)
@patch('aws_google_auth.util', spec=True)
@patch('aws_google_auth.amazon', spec=True)
@patch('aws_google_auth.google', spec=True)
def test_process_auth_with_profile(self, mock_google, mock_amazon, mock_util, mock_getpass):
def test_process_auth_with_profile(self, mock_google, mock_amazon, mock_util):

mock_config = Mock()
mock_config.saml_cache = False
Expand All @@ -320,8 +307,6 @@ def test_process_auth_with_profile(self, mock_google, mock_amazon, mock_util, mo
mock_amazon_client = Mock()
mock_google_client = Mock()

mock_getpass.return_value = "pass"

mock_amazon_client.roles = {
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps',
'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'
Expand All @@ -330,6 +315,7 @@ def test_process_auth_with_profile(self, mock_google, mock_amazon, mock_util, mo
mock_util_obj = MagicMock()
mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider"))
mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"])
mock_util_obj.get_password = MagicMock(return_value="pass")

mock_util.Util = mock_util_obj

Expand All @@ -355,13 +341,11 @@ def test_process_auth_with_profile(self, mock_google, mock_amazon, mock_util, mo
self.assertEqual([call.Util.get_input('Google username: '),
call.Util.get_input('Google IDP ID: '),
call.Util.get_input('Google SP ID: '),
call.Util.get_password('Google Password: '),
call.Util.pick_a_role({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps',
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])],
mock_util.mock_calls)

self.assertEqual([call('Google Password: ')],
mock_getpass.mock_calls)

self.assertEqual([call.do_login(), call.parse_saml()],
mock_google_client.mock_calls)

Expand All @@ -378,11 +362,10 @@ def test_process_auth_with_profile(self, mock_google, mock_amazon, mock_util, mo
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])
], mock_util_obj.pick_a_role.mock_calls)

@patch('getpass.getpass', spec=True)
@patch('aws_google_auth.util', spec=True)
@patch('aws_google_auth.amazon', spec=True)
@patch('aws_google_auth.google', spec=True)
def test_process_auth_with_saml_cache(self, mock_google, mock_amazon, mock_util, mock_getpass):
def test_process_auth_with_saml_cache(self, mock_google, mock_amazon, mock_util):

mock_config = Mock()
mock_config.saml_cache = True
Expand All @@ -396,8 +379,6 @@ def test_process_auth_with_saml_cache(self, mock_google, mock_amazon, mock_util,
mock_amazon_client = Mock()
mock_google_client = Mock()

mock_getpass.return_value = "pass"

mock_amazon_client.roles = {
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps',
'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'
Expand All @@ -406,6 +387,7 @@ def test_process_auth_with_saml_cache(self, mock_google, mock_amazon, mock_util,
mock_util_obj = MagicMock()
mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider"))
mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"])
mock_util_obj.get_password = MagicMock(return_value="pass")

mock_util.Util = mock_util_obj

Expand All @@ -432,10 +414,6 @@ def test_process_auth_with_saml_cache(self, mock_google, mock_amazon, mock_util,
'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])],
mock_util.mock_calls)

# Cache means no password request
self.assertEqual([],
mock_getpass.mock_calls)

# Cache means no google calls
self.assertEqual([],
mock_google_client.mock_calls)
Expand Down
17 changes: 17 additions & 0 deletions aws_google_auth/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
import unittest
from aws_google_auth import util
from mock import patch, MagicMock


class TestUtilMethods(unittest.TestCase):
Expand Down Expand Up @@ -46,3 +47,19 @@ def test_unicode_to_string_if_needed(self):
self.assertEqual(util.Util.unicode_to_string_if_needed(None), None)
self.assertEqual(util.Util.unicode_to_string_if_needed(1234), 1234)
self.assertEqual(util.Util.unicode_to_string_if_needed("nop"), "nop")

@patch('getpass.getpass', spec=True)
@patch('sys.stdin', spec=True)
def test_get_password_when_tty(self, mock_stdin, mock_getpass):
mock_stdin.isatty = MagicMock(return_value=True)

mock_getpass.return_value = "pass"

self.assertEqual(util.Util.get_password("Test: "), "pass")

@patch('sys.stdin', spec=True)
def test_get_password_when_not_tty(self, mock_stdin):
mock_stdin.isatty = MagicMock(return_value=False)
mock_stdin.readline = MagicMock(return_value="pass")

self.assertEqual(util.Util.get_password("Test: "), "pass")
13 changes: 13 additions & 0 deletions aws_google_auth/util.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#!/usr/bin/env python

from __future__ import print_function
import os
from collections import OrderedDict
from tabulate import tabulate
from six.moves import input
import sys
import getpass


class Util:
Expand Down Expand Up @@ -79,3 +82,13 @@ def unicode_to_string_if_needed(object):
return object.encode('utf-8')
else:
return object

@staticmethod
def get_password(prompt):
if sys.stdin.isatty():
password = getpass.getpass(prompt)
else:
print(prompt, end="")
password = sys.stdin.readline()
print("")
return password

0 comments on commit c265409

Please sign in to comment.