diff --git a/botocore/credentials.py b/botocore/credentials.py index 66b08ac86e..004ff84da7 100644 --- a/botocore/credentials.py +++ b/botocore/credentials.py @@ -46,8 +46,8 @@ def create_credential_resolver(session): config_file = session.get_config_variable('config_file') metadata_timeout = session.get_config_variable('metadata_service_timeout') num_attempts = session.get_config_variable('metadata_service_num_attempts') + providers = [ - EnvProvider(), SharedCredentialProvider( creds_filename=credential_file, profile_name=profile_name @@ -63,6 +63,18 @@ def create_credential_resolver(session): num_attempts=num_attempts) ) ] + + # We use ``session.profile`` for EnvProvider rather than + # ``profile_name`` because it is ``None`` when unset. + if session.profile is None: + # No profile has been explicitly set, so we prepend the environment + # variable provider. That provider, in turn, may set a profile + # or credentials. + providers.insert(0, EnvProvider()) + else: + logger.debug('Skipping environment variable credential check' + ' because profile name was explicitly set.') + resolver = CredentialResolver(providers=providers) return resolver @@ -580,7 +592,7 @@ def load_credentials(self): # If we got here, no credentials could be found. # This feels like it should be an exception, but historically, ``None`` # is returned. - # + # # +1 # -js return None diff --git a/tests/integration/test-credentials b/tests/integration/test-credentials new file mode 100644 index 0000000000..3bf3ae1ddf --- /dev/null +++ b/tests/integration/test-credentials @@ -0,0 +1,7 @@ +[default] +aws_access_key_id = default +aws_secret_access_key = default-secret + +[test] +aws_access_key_id = test +aws_secret_access_key = test-secret diff --git a/tests/integration/test_credentials.py b/tests/integration/test_credentials.py new file mode 100644 index 0000000000..f61d04f1f8 --- /dev/null +++ b/tests/integration/test_credentials.py @@ -0,0 +1,111 @@ +# Copyright 2015 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 os +import mock + +from botocore.session import Session +from tests import BaseEnvVar + + +class TestCredentialPrecedence(BaseEnvVar): + def setUp(self): + super(TestCredentialPrecedence, self).setUp() + + # Set the config file to something that doesn't exist so + # that we don't accidentally load a config. + os.environ['AWS_CONFIG_FILE'] = '~/.aws/config-missing' + + def create_session(self, *args, **kwargs): + """ + Create a new session with the given arguments. Additionally, + this method will set the credentials file to the test credentials + used by the following test cases. + """ + kwargs['session_vars'] = { + 'credentials_file': ( + None, None, + os.path.join(os.path.dirname(__file__), 'test-credentials')) + } + + return Session(*args, **kwargs) + + def test_access_secret_vs_profile_env(self): + # If all three are given, then the access/secret keys should + # take precedence. + os.environ['AWS_ACCESS_KEY_ID'] = 'env' + os.environ['AWS_SECRET_ACCESS_KEY'] = 'env-secret' + os.environ['BOTO_DEFAULT_PROFILE'] = 'test' + + s = self.create_session() + credentials = s.get_credentials() + + self.assertEqual(credentials.access_key, 'env') + self.assertEqual(credentials.secret_key, 'env-secret') + + @mock.patch('botocore.credentials.Credentials') + def test_access_secret_vs_profile_code(self, credentials_cls): + # If all three are given, then the access/secret keys should + # take precedence. + s = self.create_session() + s.profile = 'test' + + client = s.create_client('s3', aws_access_key_id='code', + aws_secret_access_key='code-secret') + + credentials_cls.assert_called_with( + access_key='code', secret_key='code-secret', token=mock.ANY) + + def test_profile_env_vs_code(self): + # If the profile is set both by the env var and by code, + # then the one set by code should take precedence. + os.environ['BOTO_DEFAULT_PROFILE'] = 'test' + s = self.create_session() + s.profile = 'default' + + credentials = s.get_credentials() + + self.assertEqual(credentials.access_key, 'default') + self.assertEqual(credentials.secret_key, 'default-secret') + + @mock.patch('botocore.credentials.Credentials') + def test_access_secret_env_vs_code(self, credentials_cls): + # If the access/secret keys are set both as env vars and via + # code, then those set by code should take precedence. + os.environ['AWS_ACCESS_KEY_ID'] = 'env' + os.environ['AWS_SECRET_ACCESS_KEY'] = 'secret' + s = self.create_session() + + client = s.create_client('s3', aws_access_key_id='code', + aws_secret_access_key='code-secret') + + credentials_cls.assert_called_with( + access_key='code', secret_key='code-secret', token=mock.ANY) + + def test_access_secret_env_vs_profile_code(self): + # If access/secret keys are set in the environment, but then a + # specific profile is passed via code, then the access/secret + # keys defined in that profile should take precedence over + # the environment variables. Example: + # + # ``aws --profile dev s3 ls`` + # + os.environ['AWS_ACCESS_KEY_ID'] = 'env' + os.environ['AWS_SECRET_ACCESS_KEY'] = 'env-secret' + s = self.create_session() + s.profile = 'test' + + credentials = s.get_credentials() + + self.assertEqual(credentials.access_key, 'test') + self.assertEqual(credentials.secret_key, 'test-secret') diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py index 7252f86c60..b7cfaa96d8 100644 --- a/tests/unit/test_credentials.py +++ b/tests/unit/test_credentials.py @@ -19,6 +19,7 @@ from dateutil.tz import tzlocal from botocore import credentials +from botocore.credentials import EnvProvider import botocore.exceptions import botocore.session from tests import unittest, BaseEnvVar @@ -195,7 +196,6 @@ def test_partial_creds_is_an_error(self): with self.assertRaises(botocore.exceptions.PartialCredentialsError): provider.load() - class TestSharedCredentialsProvider(BaseEnvVar): def setUp(self): super(TestSharedCredentialsProvider, self).setUp() @@ -577,9 +577,11 @@ def test_provider_unknown(self): class TestCreateCredentialResolver(BaseEnvVar): - def test_create_credential_resolver(self): - fake_session = mock.Mock() - config = { + def setUp(self): + super(TestCreateCredentialResolver, self).setUp() + + self.session = mock.Mock() + self.config = { 'credentials_file': 'a', 'legacy_config_file': 'b', 'config_file': 'c', @@ -587,10 +589,34 @@ def test_create_credential_resolver(self): 'metadata_service_num_attempts': 'e', 'profile': 'profilename', } - fake_session.get_config_variable = lambda x: config[x] - resolver = credentials.create_credential_resolver(fake_session) + self.session.get_config_variable = lambda x: self.config[x] + + def test_create_credential_resolver(self): + resolver = credentials.create_credential_resolver(self.session) self.assertIsInstance(resolver, credentials.CredentialResolver) + def test_explicit_profile_ignores_env_provider(self): + self.config['profile'] = 'dev' + resolver = credentials.create_credential_resolver(self.session) + + self.assertTrue( + all(not isinstance(p, EnvProvider) for p in resolver.providers)) + + def test_no_profile_checks_env_provider(self): + self.config['profile'] = None + self.session.profile = None + resolver = credentials.create_credential_resolver(self.session) + + self.assertTrue( + any(isinstance(p, EnvProvider) for p in resolver.providers)) + + def test_no_profile_env_provider_is_first(self): + self.config['profile'] = None + self.session.profile = None + resolver = credentials.create_credential_resolver(self.session) + + self.assertIsInstance(resolver.providers[0], credentials.EnvProvider) + if __name__ == "__main__": unittest.main()