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

Enable getting password from stdin #86

Merged
merged 8 commits into from
Aug 7, 2018
Merged
Show file tree
Hide file tree
Changes from 7 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
21 changes: 21 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,27 @@ 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.

All interactive prompt could be feeded from ``stdin``, but before `#82 <https://github.com/cevoaustralia/aws-google-auth/issues/82>`_
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feeded --> fed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅 ops! Fixed!

was not possible to feed the ``Google Password:`` prompt.

Example usage:
::
$ password-manager show password | aws-google-auth
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware of what password-manager is - is it another library?
Is it worth linking to it from here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a particular tool per se, I was meaning "whatever cli tool you are using as password manager".

I would prefer not tying the example to a specific password manager, as any cli tool that can output a password on stdout would be good.

Do you think your-password-manager-cli would be clearer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something like echo "my_password" | aws-google-auth ... would also work. People who use this tool are likely to know they can swap out the echo for anything that writes the password to stdout. Just my 2c.

Copy link
Contributor Author

@endorama endorama Aug 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

echo "my_password" |

@mide that is what I'd like to avoid. To expert users, I expect reading from stdin is a consolidated concept, as is password leaks to shell history. I'd like to avoid a sample command that's a sort of "shooting yourself in the foot" for newcomers or people not so fond on avoiding common pitfalls when using the shell.

Anyway I'm ok whit that solution as is surely clear and explicit, if someone else approves.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree 100% regarding avoiding "shoot yourself in the foot" examples, but at some point, that isn't our job. I've gone back and forth on this issue quite a bit, and I'm okay either way it lands.

I think there is value in clarity, but there is also value in preventing silly mistakes. I'm fine using a placeholder for a password manager like you've done. I can go either way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stand down on my feedback. Let's leave as is.

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