fix: add back python 2.7 for gcloud usage only (#892)

* fix: add back python 2.7 for gcloud

* fix: fix setup and tests

* fix: add enum34 for python 2.7

* fix: add app engine app and fix noxfile

* fix: move test_app_engine.py

* fix: fix downscoped

* fix: fix downscoped

* fix: remove py2 from classifiers
diff --git a/system_tests/noxfile.py b/system_tests/noxfile.py
index 540727e..459b71c 100644
--- a/system_tests/noxfile.py
+++ b/system_tests/noxfile.py
@@ -171,7 +171,7 @@
 TEST_DEPENDENCIES_ASYNC = ["aiohttp", "pytest-asyncio", "nest-asyncio"]
 TEST_DEPENDENCIES_SYNC = ["pytest", "requests", "mock"]
 PYTHON_VERSIONS_ASYNC = ["3.7"]
-PYTHON_VERSIONS_SYNC = ["3.7"]
+PYTHON_VERSIONS_SYNC = ["2.7", "3.7"]
 
 
 def default(session, *test_paths):
@@ -287,6 +287,50 @@
     )
 
 
+@nox.session(python=["2.7"])
+def app_engine(session):
+    if SKIP_GAE_TEST_ENV in os.environ:
+        session.log("Skipping App Engine tests.")
+        return
+
+    session.install(LIBRARY_DIR)
+    # Unlike the default tests above, the App Engine system test require a
+    # 'real' gcloud sdk installation that is configured to deploy to an
+    # app engine project.
+    # Grab the project ID from the cloud sdk.
+    project_id = (
+        subprocess.check_output(
+            ["gcloud", "config", "list", "project", "--format", "value(core.project)"]
+        )
+        .decode("utf-8")
+        .strip()
+    )
+
+    if not project_id:
+        session.error(
+            "The Cloud SDK must be installed and configured to deploy to App " "Engine."
+        )
+
+    application_url = GAE_APP_URL_TMPL.format(GAE_TEST_APP_SERVICE, project_id)
+
+    # Vendor in the test application's dependencies
+    session.chdir(os.path.join(HERE, "system_tests_sync/app_engine_test_app"))
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.run(
+        "pip", "install", "--target", "lib", "-r", "requirements.txt", silent=True
+    )
+
+    # Deploy the application.
+    session.run("gcloud", "app", "deploy", "-q", "app.yaml")
+
+    # Run the tests
+    session.env["TEST_APP_URL"] = application_url
+    session.chdir(HERE)
+    default(
+        session, "system_tests_sync/test_app_engine.py",
+    )
+
+
 @nox.session(python=PYTHON_VERSIONS_SYNC)
 def grpc(session):
     session.install(LIBRARY_DIR)
@@ -339,9 +383,8 @@
 def external_accounts(session):
     session.install(
         *TEST_DEPENDENCIES_SYNC,
-        "google-auth",
+        LIBRARY_DIR,
         "google-api-python-client",
-        "enum34",
     )
     default(
         session,
@@ -354,7 +397,7 @@
 def downscoping(session):
     session.install(
         *TEST_DEPENDENCIES_SYNC,
-        "google-auth",
+        LIBRARY_DIR,
         "google-cloud-storage",
     )
     default(
diff --git a/system_tests/system_tests_sync/app_engine_test_app/.gitignore b/system_tests/system_tests_sync/app_engine_test_app/.gitignore
new file mode 100644
index 0000000..7951405
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/.gitignore
@@ -0,0 +1 @@
+lib
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/app_engine_test_app/app.yaml b/system_tests/system_tests_sync/app_engine_test_app/app.yaml
new file mode 100644
index 0000000..06f2270
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/app.yaml
@@ -0,0 +1,12 @@
+api_version: 1
+service: google-auth-system-tests
+runtime: python27
+threadsafe: true
+
+handlers:
+- url: .*
+  script: main.app
+
+libraries:
+- name: ssl
+  version: 2.7.11
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/app_engine_test_app/appengine_config.py b/system_tests/system_tests_sync/app_engine_test_app/appengine_config.py
new file mode 100644
index 0000000..1197ab5
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/appengine_config.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License 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.
+
+from google.appengine.ext import vendor
+
+# Add any libraries installed in the "lib" folder.
+vendor.add("lib")
+
+
+# Patch os.path.expanduser. This should be fixed in GAE
+# versions released after Nov 2016.
+import os.path
+
+
+def patched_expanduser(path):
+    return path
+
+
+os.path.expanduser = patched_expanduser
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/app_engine_test_app/main.py b/system_tests/system_tests_sync/app_engine_test_app/main.py
new file mode 100644
index 0000000..f44ed4c
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/main.py
@@ -0,0 +1,129 @@
+# Copyright 2016 Google LLC 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.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License 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.
+
+"""App Engine standard application that runs basic system tests for
+google.auth.app_engine.
+This application has to run tests manually instead of using pytest because
+pytest currently doesn't work on App Engine standard.
+"""
+
+import contextlib
+import json
+import sys
+from StringIO import StringIO
+import traceback
+
+from google.appengine.api import app_identity
+import google.auth
+from google.auth import _helpers
+from google.auth import app_engine
+import google.auth.transport.urllib3
+import urllib3.contrib.appengine
+import webapp2
+
+FAILED_TEST_TMPL = """
+Test {} failed: {}
+Stacktrace:
+{}
+Captured output:
+{}
+"""
+TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v3/tokeninfo"
+EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.email"
+HTTP = urllib3.contrib.appengine.AppEngineManager()
+HTTP_REQUEST = google.auth.transport.urllib3.Request(HTTP)
+
+
+def test_credentials():
+    credentials = app_engine.Credentials()
+    scoped_credentials = credentials.with_scopes([EMAIL_SCOPE])
+
+    scoped_credentials.refresh(None)
+
+    assert scoped_credentials.valid
+    assert scoped_credentials.token is not None
+
+    # Get token info and verify scope
+    url = _helpers.update_query(
+        TOKEN_INFO_URL, {"access_token": scoped_credentials.token}
+    )
+    response = HTTP_REQUEST(url=url, method="GET")
+    token_info = json.loads(response.data.decode("utf-8"))
+
+    assert token_info["scope"] == EMAIL_SCOPE
+
+
+def test_default():
+    credentials, project_id = google.auth.default()
+
+    assert isinstance(credentials, app_engine.Credentials)
+    assert project_id == app_identity.get_application_id()
+
+
+@contextlib.contextmanager
+def capture():
+    """Context manager that captures stderr and stdout."""
+    oldout, olderr = sys.stdout, sys.stderr
+    try:
+        out = StringIO()
+        sys.stdout, sys.stderr = out, out
+        yield out
+    finally:
+        sys.stdout, sys.stderr = oldout, olderr
+
+
+def run_test_func(func):
+    with capture() as capsys:
+        try:
+            func()
+            return True, ""
+        except Exception as exc:
+            output = FAILED_TEST_TMPL.format(
+                func.func_name, exc, traceback.format_exc(), capsys.getvalue()
+            )
+            return False, output
+
+
+def run_tests():
+    """Runs all tests.
+    Returns:
+        Tuple[bool, str]: A tuple containing True if all tests pass, False
+        otherwise, and any captured output from the tests.
+    """
+    status = True
+    output = ""
+
+    tests = (test_credentials, test_default)
+
+    for test in tests:
+        test_status, test_output = run_test_func(test)
+        status = status and test_status
+        output += test_output
+
+    return status, output
+
+
+class MainHandler(webapp2.RequestHandler):
+    def get(self):
+        self.response.headers["content-type"] = "text/plain"
+
+        status, output = run_tests()
+
+        if not status:
+            self.response.status = 500
+
+        self.response.write(output)
+
+
+app = webapp2.WSGIApplication([("/", MainHandler)], debug=True)
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/app_engine_test_app/requirements.txt b/system_tests/system_tests_sync/app_engine_test_app/requirements.txt
new file mode 100644
index 0000000..cb8a382
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/requirements.txt
@@ -0,0 +1,3 @@
+urllib3
+# Relative path to google-auth-python's source.
+../../..
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/test_app_engine.py b/system_tests/system_tests_sync/test_app_engine.py
new file mode 100644
index 0000000..79776ce
--- /dev/null
+++ b/system_tests/system_tests_sync/test_app_engine.py
@@ -0,0 +1,22 @@
+# Copyright 2016 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License 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
+
+TEST_APP_URL = os.environ["TEST_APP_URL"]
+
+
+def test_live_application(http_request):
+    response = http_request(method="GET", url=TEST_APP_URL)
+    assert response.status == 200, response.data.decode("utf-8")
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/test_downscoping.py b/system_tests/system_tests_sync/test_downscoping.py
index 77224ae..fdb4efa 100644
--- a/system_tests/system_tests_sync/test_downscoping.py
+++ b/system_tests/system_tests_sync/test_downscoping.py
@@ -28,7 +28,7 @@
  # The object prefix used to test access to files beginning with this prefix.
 _OBJECT_PREFIX = "customer-a"
 # The object name of the object inaccessible by the downscoped token.
-_ACCESSIBLE_OBJECT_NAME = f"{_OBJECT_PREFIX}-data.txt"
+_ACCESSIBLE_OBJECT_NAME = "{0}-data.txt".format(_OBJECT_PREFIX)
 # The content of the object accessible by the downscoped token.
 _ACCESSIBLE_CONTENT = "hello world"
 # The content of the object inaccessible by the downscoped token.
@@ -76,13 +76,13 @@
         Tuple[str, datetime.datetime]: The downscoped access token and its expiry date.
     """
     # Initialize the Credential Access Boundary rules.
-    available_resource = f"//storage.googleapis.com/projects/_/buckets/{bucket_name}"
+    available_resource = "//storage.googleapis.com/projects/_/buckets/{0}".format(bucket_name)
     # Downscoped credentials will have readonly access to the resource.
     available_permissions = ["inRole:roles/storage.objectViewer"]
     # Only objects starting with the specified prefix string in the object name
     # will be allowed read access.
     availability_expression = (
-        f"resource.name.startsWith('projects/_/buckets/{bucket_name}/objects/{object_prefix}')"
+        "resource.name.startsWith('projects/_/buckets/{0}/objects/{1}')".format(bucket_name, object_prefix)
     )
     availability_condition = downscoped.AvailabilityCondition(availability_expression)
     # Define the single access boundary rule using the above properties.
diff --git a/system_tests/system_tests_sync/test_external_accounts.py b/system_tests/system_tests_sync/test_external_accounts.py
index c2855a2..e24c7b4 100644
--- a/system_tests/system_tests_sync/test_external_accounts.py
+++ b/system_tests/system_tests_sync/test_external_accounts.py
@@ -32,21 +32,19 @@
 # original service account key.
 
 
-from http.server import BaseHTTPRequestHandler
-from http.server import HTTPServer
 import json
 import os
 import socket
-import sys
 from tempfile import NamedTemporaryFile
 import threading
 
-import pytest
-from mock import patch
-
+import sys
 import google.auth
 from googleapiclient import discovery
+from six.moves import BaseHTTPServer
 from google.oauth2 import service_account
+import pytest
+from mock import patch
 
 # Populate values from the output of scripts/setup_external_accounts.sh.
 _AUDIENCE_OIDC = "//iam.googleapis.com/projects/79992041559/locations/global/workloadIdentityPools/pool-73wslmxn/providers/oidc-73wslmxn"
@@ -177,7 +175,7 @@
 # This test makes sure that setting up an http server to provide credentials
 # works to allow access to Google resources.
 def test_url_based_external_account(dns_access, oidc_credentials, service_account_info):
-    class TestResponseHandler(BaseHTTPRequestHandler):
+    class TestResponseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         def do_GET(self):
             if self.headers["my-header"] != "expected-value":
                 self.send_response(400)
@@ -201,7 +199,7 @@
                     json.dumps({"access_token": oidc_credentials.token}).encode("utf-8")
                 )
 
-    class TestHTTPServer(HTTPServer, object):
+    class TestHTTPServer(BaseHTTPServer.HTTPServer, object):
         def __init__(self):
             self.port = self._find_open_port()
             super(TestHTTPServer, self).__init__(("", self.port), TestResponseHandler)