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

Allow stderr passthrough for credential_process #1835

Closed
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
28 changes: 23 additions & 5 deletions botocore/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import threading
import json
import subprocess
import sys
from collections import namedtuple
from copy import deepcopy
from hashlib import sha1
Expand Down Expand Up @@ -906,11 +907,13 @@ class ProcessProvider(CredentialProvider):

METHOD = 'custom-process'

def __init__(self, profile_name, load_config, popen=subprocess.Popen):
def __init__(self, profile_name, load_config,
popen=subprocess.Popen, stderr=sys.stderr):
self._profile_name = profile_name
self._load_config = load_config
self._loaded_config = None
self._popen = popen
self._stderr = stderr

def load(self):
credential_process = self._credential_process
Expand All @@ -936,14 +939,29 @@ def _retrieve_credentials_using(self, credential_process):
# We're not using shell=True, so we need to pass the
# command and all arguments as a list.
process_list = compat_shell_split(credential_process)

# If stderr is a tty, we won't capture stderr for error reporting,
# because we assume stderr is being used for communication back
# to the user, e.g. for MFA, passwords, or other prompting.
if self._stderr and self._stderr.isatty():
stderr = self._stderr
else:
stderr = subprocess.PIPE

p = self._popen(process_list,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
stderr=stderr)

output, error_output = p.communicate()
if p.returncode != 0:
if error_output == None:
error_msg = "Non-zero exit code"
else:
error_msg = error_output.decode('utf-8')
raise CredentialRetrievalError(
provider=self.METHOD, error_msg=stderr.decode('utf-8'))
parsed = botocore.compat.json.loads(stdout.decode('utf-8'))
provider=self.METHOD, error_msg=error_msg)

parsed = botocore.compat.json.loads(output.decode('utf-8'))
version = parsed.get('Version', '<Version key not provided>')
if version != 1:
raise CredentialRetrievalError(
Expand Down
52 changes: 50 additions & 2 deletions tests/unit/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import shutil
import json
import copy
import sys

from dateutil.tz import tzlocal, tzutc

Expand Down Expand Up @@ -2965,14 +2966,24 @@ def setUp(self):
self.invoked_process = mock.Mock()
self.popen_mock = mock.Mock(return_value=self.invoked_process,
spec=subprocess.Popen)
self.tty = False
self.stderr_mock = mock.Mock(spec=sys.stderr)
self.stderr_mock.isatty.side_effect = lambda: self.tty

def create_process_provider(self, profile_name='default'):
provider = ProcessProvider(profile_name, self.load_config,
popen=self.popen_mock)
popen=self.popen_mock,
stderr=self.stderr_mock)
return provider

def _get_output(self, stdout, stderr=''):
return json.dumps(stdout).encode('utf-8'), stderr.encode('utf-8')
if self.tty:
return json.dumps(stdout).encode('utf-8'), None
else:
return json.dumps(stdout).encode('utf-8'), stderr.encode('utf-8')

def _set_tty(self, tty):
self.tty = tty

def _set_process_return_value(self, stdout, stderr='', rc=0):
output = self._get_output(stdout, stderr)
Expand Down Expand Up @@ -3015,6 +3026,31 @@ def test_can_retrieve_via_process(self):
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

def test_can_retrieve_via_process_passes_through_tty(self):
self.loaded_config['profiles'] = {
'default': {'credential_process': 'my-process'}
}
self._set_tty(True)
self._set_process_return_value({
'Version': 1,
'AccessKeyId': 'foo',
'SecretAccessKey': 'bar',
'SessionToken': 'baz',
'Expiration': '2999-01-01T00:00:00Z',
})

provider = self.create_process_provider()
creds = provider.load()
self.assertIsNotNone(creds)
self.assertEqual(creds.access_key, 'foo')
self.assertEqual(creds.secret_key, 'bar')
self.assertEqual(creds.token, 'baz')
self.assertEqual(creds.method, 'custom-process')
self.popen_mock.assert_called_with(
['my-process'],
stdout=subprocess.PIPE, stderr=self.stderr_mock,
)

def test_can_pass_arguments_through(self):
self.loaded_config['profiles'] = {
'default': {
Expand Down Expand Up @@ -3083,6 +3119,18 @@ def test_non_zero_rc_raises_exception(self):
with self.assertRaisesRegexp(exception, 'Error Message'):
provider.load()

def test_non_zero_rc_with_tty_raises_non_zero_exception(self):
self.loaded_config['profiles'] = {
'default': {'credential_process': 'my-process'}
}
self._set_tty(True)
self._set_process_return_value('', 'Error Message', 1)

provider = self.create_process_provider()
exception = botocore.exceptions.CredentialRetrievalError
with self.assertRaisesRegexp(exception, 'Non-zero exit code'):
provider.load()

def test_unsupported_version_raises_mismatch(self):
self.loaded_config['profiles'] = {
'default': {'credential_process': 'my-process'}
Expand Down