Repository: boto/botocore
Issue: Environment variables set to empty cause silent failing of auth

Hello,

recently, we stumbled upon an interesting 'bug' that took us some time to resolve - we had set `AWS_ACCESS_KEY_ID` env variable set to empty, which resulted of having it in `os.environ` as empty string. If it's there as an empty string, it causes config resolver to use EnvProvider - even if it's clearly invalid. While i realize that this issue is caused by us, I would like to propose a small improvement to EnvProvider - which will not only check if env variables are set - but also if they're valid and log/error out when that's not the case, allowing users for quicker debugging. 

If I can help with the implementation, please let me know, I would gladly take a stab at it.

Regards,
Piotr

Changed files:
tests/unit/test_credentials.py, botocore/credentials.py

diff --git a/botocore/credentials.py b/botocore/credentials.py
index 19b12b327..51e301f0b 100644
--- a/botocore/credentials.py
+++ b/botocore/credentials.py
@@ -921,7 +921,10 @@ class EnvProvider(CredentialProvider):
         """
         Search for credentials in explicit environment variables.
         """
-        if self._mapping['access_key'] in self.environ:
+
+        access_key_var = self._mapping['access_key']
+
+        if access_key_var in self.environ and not self.environ.get(access_key_var) == '':
             logger.info('Found credentials in environment variables.')
             fetcher = self._create_credentials_fetcher()
             credentials = fetcher(require_expiry=False)
@@ -951,20 +954,20 @@ class EnvProvider(CredentialProvider):
             credentials = {}
 
             access_key = environ.get(mapping['access_key'])
-            if access_key is None:
+            if access_key is None or access_key == '':
                 raise PartialCredentialsError(
                     provider=method, cred_var=mapping['access_key'])
             credentials['access_key'] = access_key
 
             secret_key = environ.get(mapping['secret_key'])
-            if secret_key is None:
+            if secret_key is None or secret_key == '':
                 raise PartialCredentialsError(
                     provider=method, cred_var=mapping['secret_key'])
             credentials['secret_key'] = secret_key
 
             token = None
             for token_env_var in mapping['token']:
-                if token_env_var in environ:
+                if token_env_var in environ and not environ[token_env_var] == '':
                     token = environ[token_env_var]
                     break
             credentials['token'] = token
diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py
index 12caa4b09..796cdab9a 100644
--- a/tests/unit/test_credentials.py
+++ b/tests/unit/test_credentials.py
@@ -691,6 +691,16 @@ class TestEnvVar(BaseEnvVar):
         creds = provider.load()
         self.assertIsNone(creds)
 
+    def test_envvars_empty_string(self):
+        environ = {
+            'AWS_ACCESS_KEY_ID': '',
+            'AWS_SECRET_ACCESS_KEY': '',
+            'AWS_SECURITY_TOKEN': '',
+        }
+        provider = credentials.EnvProvider(environ)
+        creds = provider.load()
+        self.assertIsNone(creds)
+
     def test_can_override_env_var_mapping(self):
         # We can change the env var provider to
         # use our specified env var names.
@@ -765,6 +775,18 @@ class TestEnvVar(BaseEnvVar):
         with self.assertRaises(botocore.exceptions.PartialCredentialsError):
             provider.load()
 
+    def test_partial_creds_is_an_error_empty_string(self):
+        # If the user provides an access key, they must also
+        # provide a secret key.  Not doing so will generate an
+        # error.
+        environ = {
+            'AWS_ACCESS_KEY_ID': 'foo',
+            'AWS_SECRET_ACCESS_KEY': '',
+        }
+        provider = credentials.EnvProvider(environ)
+        with self.assertRaises(botocore.exceptions.PartialCredentialsError):
+            provider.load()
+
     def test_missing_access_key_id_raises_error(self):
         expiry_time = datetime.now(tzlocal()) - timedelta(hours=1)
         environ = {