diff --git a/README.rst b/README.rst index b4f97a5..f9b393f 100644 --- a/README.rst +++ b/README.rst @@ -152,6 +152,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 `_, 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 ------------------------------ diff --git a/aws_google_auth/__init__.py b/aws_google_auth/__init__.py index 06ede8b..c332bb3 100644 --- a/aws_google_auth/__init__.py +++ b/aws_google_auth/__init__.py @@ -8,7 +8,6 @@ from . import amazon import argparse -import getpass import keyring import os import sys @@ -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() diff --git a/aws_google_auth/tests/test_init.py b/aws_google_auth/tests/test_init.py index fdd7dcc..9c4901f 100644 --- a/aws_google_auth/tests/test_init.py +++ b/aws_google_auth/tests/test_init.py @@ -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 @@ -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' @@ -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 @@ -129,6 +127,7 @@ 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) @@ -136,9 +135,6 @@ def test_process_auth_standard(self, mock_google, mock_amazon, mock_util, mock_g 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) @@ -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 @@ -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' @@ -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 @@ -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) @@ -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 @@ -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' @@ -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 @@ -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) @@ -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 @@ -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' @@ -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 @@ -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) @@ -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 @@ -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' @@ -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 @@ -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) diff --git a/aws_google_auth/tests/test_util.py b/aws_google_auth/tests/test_util.py index eafee23..c92ff0f 100644 --- a/aws_google_auth/tests/test_util.py +++ b/aws_google_auth/tests/test_util.py @@ -3,6 +3,7 @@ import sys import unittest from aws_google_auth import util +from mock import patch, MagicMock class TestUtilMethods(unittest.TestCase): @@ -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") diff --git a/aws_google_auth/util.py b/aws_google_auth/util.py index dd19ba2..38f8b1b 100644 --- a/aws_google_auth/util.py +++ b/aws_google_auth/util.py @@ -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: @@ -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