Repository: googleapis/google-auth-library-python

Issue:
Add App Engine credentials to Application Default Credentials



Discussion:

Resolving patch:
diff --git a/google/auth/_default.py b/google/auth/_default.py
--- a/google/auth/_default.py
+++ b/google/auth/_default.py
@@ -23,6 +23,7 @@
 import os
 
 from google.auth import _cloud_sdk
+from google.auth import app_engine
 from google.auth import compute_engine
 from google.auth import environment_vars
 from google.auth import exceptions
@@ -150,7 +151,12 @@ def _get_explicit_environ_credentials():
 
 def _get_gae_credentials():
     """Gets Google App Engine App Identity credentials and project ID."""
-    return None, None
+    try:
+        credentials = app_engine.Credentials()
+        project_id = app_engine.get_project_id()
+        return credentials, project_id
+    except EnvironmentError:
+        return None, None
 
 
 def _get_gce_credentials(request=None):
@@ -166,7 +172,7 @@ def _get_gce_credentials(request=None):
     if _metadata.ping(request=request):
         # Get the project ID.
         try:
-            project_id = _metadata.get(request, 'project/project-id')
+            project_id = _metadata.get_project_id(request=request)
         except exceptions.TransportError:
             _LOGGER.warning(
                 'No project ID could be determined from the Compute Engine '
diff --git a/google/auth/app_engine.py b/google/auth/app_engine.py
--- a/google/auth/app_engine.py
+++ b/google/auth/app_engine.py
@@ -33,6 +33,21 @@
     app_identity = None
 
 
+def get_project_id():
+    """Gets the project ID for the current App Engine application.
+
+    Returns:
+        str: The project ID
+
+    Raises:
+        EnvironmentError: If the App Engine APIs are unavailable.
+    """
+    if app_identity is None:
+        raise EnvironmentError(
+            'The App Engine APIs are not available.')
+    return app_identity.get_application_id()
+
+
 class Credentials(credentials.Scoped, credentials.Signing,
                   credentials.Credentials):
     """App Engine standard environment credentials.
diff --git a/google/auth/compute_engine/_metadata.py b/google/auth/compute_engine/_metadata.py
--- a/google/auth/compute_engine/_metadata.py
+++ b/google/auth/compute_engine/_metadata.py
@@ -128,6 +128,23 @@ def get(request, path, root=_METADATA_ROOT, recursive=False):
                 url, response.status, response.data), response)
 
 
+def get_project_id(request):
+    """Get the Google Cloud Project ID from the metadata server.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+
+    Returns:
+        str: The project ID
+
+    Raises:
+        google.auth.exceptions.TransportError: if an error occurred while
+            retrieving metadata.
+    """
+    return get(request, 'project/project-id')
+
+
 def get_service_account_info(request, service_account='default'):
     """Get information about a service account from the metadata server.
 


Test patch:
diff --git a/tests/compute_engine/test__metadata.py b/tests/compute_engine/test__metadata.py
--- a/tests/compute_engine/test__metadata.py
+++ b/tests/compute_engine/test__metadata.py
@@ -126,6 +126,20 @@ def test_get_failure_bad_json(mock_request):
         headers=_metadata._METADATA_HEADERS)
 
 
+def test_get_project_id(mock_request):
+    project = 'example-project'
+    request_mock = mock_request(
+        project, headers={'content-type': 'text/plain'})
+
+    project_id = _metadata.get_project_id(request_mock)
+
+    request_mock.assert_called_once_with(
+        method='GET',
+        url=_metadata._METADATA_ROOT + 'project/project-id',
+        headers=_metadata._METADATA_HEADERS)
+    assert project_id == project
+
+
 @mock.patch('google.auth._helpers.utcnow', return_value=datetime.datetime.min)
 def test_get_service_account_token(utcnow, mock_request):
     ttl = 500
diff --git a/tests/test__default.py b/tests/test__default.py
--- a/tests/test__default.py
+++ b/tests/test__default.py
@@ -19,6 +19,7 @@
 import pytest
 
 from google.auth import _default
+from google.auth import app_engine
 from google.auth import compute_engine
 from google.auth import environment_vars
 from google.auth import exceptions
@@ -188,14 +189,33 @@ def test__get_gcloud_sdk_credentials_no_project_id(
     assert project_id is None
 
 
-def test__get_gae_credentials():
+@pytest.fixture
+def app_identity_mock(monkeypatch):
+    """Mocks the app_identity module for google.auth.app_engine."""
+    app_identity_mock = mock.Mock()
+    monkeypatch.setattr(
+        app_engine, 'app_identity', app_identity_mock)
+    yield app_identity_mock
+
+
+def test__get_gae_credentials(app_identity_mock):
+    app_identity_mock.get_application_id.return_value = mock.sentinel.project
+
+    credentials, project_id = _default._get_gae_credentials()
+
+    assert isinstance(credentials, app_engine.Credentials)
+    assert project_id == mock.sentinel.project
+
+
+def test__get_gae_credentials_no_apis():
     assert _default._get_gae_credentials() == (None, None)
 
 
 @mock.patch(
     'google.auth.compute_engine._metadata.ping', return_value=True)
 @mock.patch(
-    'google.auth.compute_engine._metadata.get', return_value='example-project')
+    'google.auth.compute_engine._metadata.get_project_id',
+    return_value='example-project')
 def test__get_gce_credentials(get_mock, ping_mock):
     credentials, project_id = _default._get_gce_credentials()
 
@@ -214,7 +234,7 @@ def test__get_gce_credentials_no_ping(ping_mock):
 @mock.patch(
     'google.auth.compute_engine._metadata.ping', return_value=True)
 @mock.patch(
-    'google.auth.compute_engine._metadata.get',
+    'google.auth.compute_engine._metadata.get_project_id',
     side_effect=exceptions.TransportError())
 def test__get_gce_credentials_no_project_id(get_mock, ping_mock):
     credentials, project_id = _default._get_gce_credentials()
diff --git a/tests/test_app_engine.py b/tests/test_app_engine.py
--- a/tests/test_app_engine.py
+++ b/tests/test_app_engine.py
@@ -29,6 +29,18 @@ def app_identity_mock(monkeypatch):
     yield app_identity_mock
 
 
+def test_get_project_id(app_identity_mock):
+    app_identity_mock.get_application_id.return_value = mock.sentinel.project
+    assert app_engine.get_project_id() == mock.sentinel.project
+
+
+def test_get_project_id_missing_apis():
+    with pytest.raises(EnvironmentError) as excinfo:
+        assert app_engine.get_project_id()
+
+    assert excinfo.match(r'App Engine APIs are not available')
+
+
 class TestCredentials(object):
     def test_missing_apis(self):
         with pytest.raises(EnvironmentError) as excinfo: