aboutsummaryrefslogtreecommitdiff
path: root/system_tests
diff options
context:
space:
mode:
Diffstat (limited to 'system_tests')
-rw-r--r--system_tests/__init__.py0
-rw-r--r--system_tests/noxfile.py498
-rw-r--r--system_tests/secrets.tar.encbin0 -> 10323 bytes
-rw-r--r--system_tests/system_tests_async/__init__.py0
-rw-r--r--system_tests/system_tests_async/conftest.py115
-rw-r--r--system_tests/system_tests_async/test_default.py29
-rw-r--r--system_tests/system_tests_async/test_id_token.py25
-rw-r--r--system_tests/system_tests_async/test_service_account.py53
-rw-r--r--system_tests/system_tests_sync/.gitignore2
-rw-r--r--system_tests/system_tests_sync/__init__.py0
-rw-r--r--system_tests/system_tests_sync/app_engine_test_app/.gitignore1
-rw-r--r--system_tests/system_tests_sync/app_engine_test_app/app.yaml12
-rw-r--r--system_tests/system_tests_sync/app_engine_test_app/appengine_config.py30
-rw-r--r--system_tests/system_tests_sync/app_engine_test_app/main.py129
-rw-r--r--system_tests/system_tests_sync/app_engine_test_app/requirements.txt3
-rw-r--r--system_tests/system_tests_sync/conftest.py141
-rw-r--r--system_tests/system_tests_sync/secrets.tar.encbin0 -> 10323 bytes
-rw-r--r--system_tests/system_tests_sync/test_app_engine.py22
-rw-r--r--system_tests/system_tests_sync/test_compute_engine.py75
-rw-r--r--system_tests/system_tests_sync/test_default.py28
-rw-r--r--system_tests/system_tests_sync/test_downscoping.py162
-rw-r--r--system_tests/system_tests_sync/test_external_accounts.py305
-rw-r--r--system_tests/system_tests_sync/test_grpc.py93
-rw-r--r--system_tests/system_tests_sync/test_id_token.py25
-rw-r--r--system_tests/system_tests_sync/test_impersonated_credentials.py99
-rw-r--r--system_tests/system_tests_sync/test_mtls_http.py124
-rw-r--r--system_tests/system_tests_sync/test_oauth2_credentials.py55
-rw-r--r--system_tests/system_tests_sync/test_requests.py42
-rw-r--r--system_tests/system_tests_sync/test_service_account.py65
-rw-r--r--system_tests/system_tests_sync/test_urllib3.py44
30 files changed, 2177 insertions, 0 deletions
diff --git a/system_tests/__init__.py b/system_tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/system_tests/__init__.py
diff --git a/system_tests/noxfile.py b/system_tests/noxfile.py
new file mode 100644
index 0000000..459b71c
--- /dev/null
+++ b/system_tests/noxfile.py
@@ -0,0 +1,498 @@
+# Copyright 2020 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.
+
+"""Noxfile for automating system tests.
+
+This file handles setting up environments needed by the system tests. This
+separates the tests from their environment configuration.
+
+See the `nox docs`_ for details on how this file works:
+
+.. _nox docs: http://nox.readthedocs.io/en/latest/
+"""
+
+import os
+import subprocess
+
+from nox.command import which
+import nox
+import py.path
+
+HERE = os.path.abspath(os.path.dirname(__file__))
+LIBRARY_DIR = os.path.abspath(os.path.dirname(HERE))
+DATA_DIR = os.path.join(HERE, "data")
+SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
+AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
+EXPLICIT_CREDENTIALS_ENV = "GOOGLE_APPLICATION_CREDENTIALS"
+EXPLICIT_PROJECT_ENV = "GOOGLE_CLOUD_PROJECT"
+EXPECT_PROJECT_ENV = "EXPECT_PROJECT_ID"
+
+SKIP_GAE_TEST_ENV = "SKIP_APP_ENGINE_SYSTEM_TEST"
+GAE_APP_URL_TMPL = "https://{}-dot-{}.appspot.com"
+GAE_TEST_APP_SERVICE = "google-auth-system-tests"
+
+# The download location for the Cloud SDK
+CLOUD_SDK_DIST_FILENAME = "google-cloud-sdk.tar.gz"
+CLOUD_SDK_DOWNLOAD_URL = "https://dl.google.com/dl/cloudsdk/release/{}".format(
+ CLOUD_SDK_DIST_FILENAME
+)
+
+# This environment variable is recognized by the Cloud SDK and overrides
+# the location of the SDK's configuration files (which is usually at
+# ${HOME}/.config).
+CLOUD_SDK_CONFIG_ENV = "CLOUDSDK_CONFIG"
+
+# If set, this is where the environment setup will install the Cloud SDK.
+# If unset, it will download the SDK to a temporary directory.
+CLOUD_SDK_ROOT = os.environ.get("CLOUD_SDK_ROOT")
+
+if CLOUD_SDK_ROOT is not None:
+ CLOUD_SDK_ROOT = py.path.local(CLOUD_SDK_ROOT)
+ CLOUD_SDK_ROOT.ensure(dir=True) # Makes sure the directory exists.
+else:
+ CLOUD_SDK_ROOT = py.path.local.mkdtemp()
+
+# The full path the cloud sdk install directory
+CLOUD_SDK_INSTALL_DIR = CLOUD_SDK_ROOT.join("google-cloud-sdk")
+
+# The full path to the gcloud cli executable.
+GCLOUD = str(CLOUD_SDK_INSTALL_DIR.join("bin", "gcloud"))
+
+# gcloud requires Python 2 and doesn't work on 3, so we need to tell it
+# where to find 2 when we're running in a 3 environment.
+CLOUD_SDK_PYTHON_ENV = "CLOUDSDK_PYTHON"
+CLOUD_SDK_PYTHON = which("python2", None)
+
+# Cloud SDK helpers
+
+
+def install_cloud_sdk(session):
+ """Downloads and installs the Google Cloud SDK."""
+ # Configure environment variables needed by the SDK.
+ # This sets the config root to the tests' config root. This prevents
+ # our tests from clobbering a developer's configuration when running
+ # these tests locally.
+ session.env[CLOUD_SDK_CONFIG_ENV] = str(CLOUD_SDK_ROOT)
+ # This tells gcloud which Python interpreter to use (always use 2.7)
+ session.env[CLOUD_SDK_PYTHON_ENV] = CLOUD_SDK_PYTHON
+ # This set the $PATH for the subprocesses so they can find the gcloud
+ # executable.
+ session.env["PATH"] = (
+ str(CLOUD_SDK_INSTALL_DIR.join("bin")) + os.pathsep + os.environ["PATH"]
+ )
+
+ # If gcloud cli executable already exists, just update it.
+ if py.path.local(GCLOUD).exists():
+ session.run(GCLOUD, "components", "update", "-q")
+ return
+
+ tar_path = CLOUD_SDK_ROOT.join(CLOUD_SDK_DIST_FILENAME)
+
+ # Download the release.
+ session.run("wget", CLOUD_SDK_DOWNLOAD_URL, "-O", str(tar_path), silent=True)
+
+ # Extract the release.
+ session.run("tar", "xzf", str(tar_path), "-C", str(CLOUD_SDK_ROOT))
+ session.run(tar_path.remove)
+
+ # Run the install script.
+ session.run(
+ str(CLOUD_SDK_INSTALL_DIR.join("install.sh")),
+ "--usage-reporting",
+ "false",
+ "--path-update",
+ "false",
+ "--command-completion",
+ "false",
+ silent=True,
+ )
+
+
+def copy_credentials(credentials_path):
+ """Copies credentials into the SDK root as the application default
+ credentials."""
+ dest = CLOUD_SDK_ROOT.join("application_default_credentials.json")
+ if dest.exists():
+ dest.remove()
+ py.path.local(credentials_path).copy(dest)
+
+
+def configure_cloud_sdk(session, application_default_credentials, project=False):
+ """Installs and configures the Cloud SDK with the given application default
+ credentials.
+
+ If project is True, then a project will be set in the active config.
+ If it is false, this will ensure no project is set.
+ """
+ install_cloud_sdk(session)
+
+ # Setup the service account as the default user account. This is
+ # needed for the project ID detection to work. Note that this doesn't
+ # change the application default credentials file, which is user
+ # credentials instead of service account credentials sometimes.
+ session.run(
+ GCLOUD, "auth", "activate-service-account", "--key-file", SERVICE_ACCOUNT_FILE
+ )
+
+ if project:
+ session.run(GCLOUD, "config", "set", "project", "example-project")
+ else:
+ session.run(GCLOUD, "config", "unset", "project")
+
+ # Copy the credentials file to the config root. This is needed because
+ # unfortunately gcloud doesn't provide a clean way to tell it to use
+ # a particular set of credentials. However, this does verify that gcloud
+ # also considers the credentials valid by calling application-default
+ # print-access-token
+ session.run(copy_credentials, application_default_credentials)
+
+ # Calling this forces the Cloud SDK to read the credentials we just wrote
+ # and obtain a new access token with those credentials. This validates
+ # that our credentials matches the format expected by gcloud.
+ # Silent is set to True to prevent leaking secrets in test logs.
+ session.run(
+ GCLOUD, "auth", "application-default", "print-access-token", silent=True
+ )
+
+
+# Test sesssions
+
+TEST_DEPENDENCIES_ASYNC = ["aiohttp", "pytest-asyncio", "nest-asyncio"]
+TEST_DEPENDENCIES_SYNC = ["pytest", "requests", "mock"]
+PYTHON_VERSIONS_ASYNC = ["3.7"]
+PYTHON_VERSIONS_SYNC = ["2.7", "3.7"]
+
+
+def default(session, *test_paths):
+ # replace 'session._runner.friendly_name' with
+ # session.name once nox has released a new version
+ # https://github.com/theacodes/nox/pull/386
+ sponge_log = f"--junitxml=system_{str(session._runner.friendly_name)}_sponge_log.xml"
+ session.run(
+ "pytest", sponge_log, *test_paths,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def service_account_sync(session):
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_sync/test_service_account.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def default_explicit_service_account(session):
+ session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+ session.env[EXPECT_PROJECT_ENV] = "1"
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_sync/test_default.py",
+ "system_tests_sync/test_id_token.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def default_explicit_authorized_user(session):
+ session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_sync/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def default_explicit_authorized_user_explicit_project(session):
+ session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
+ session.env[EXPLICIT_PROJECT_ENV] = "example-project"
+ session.env[EXPECT_PROJECT_ENV] = "1"
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_sync/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def default_cloud_sdk_service_account(session):
+ configure_cloud_sdk(session, SERVICE_ACCOUNT_FILE)
+ session.env[EXPECT_PROJECT_ENV] = "1"
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_sync/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def default_cloud_sdk_authorized_user(session):
+ configure_cloud_sdk(session, AUTHORIZED_USER_FILE)
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_sync/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def default_cloud_sdk_authorized_user_configured_project(session):
+ configure_cloud_sdk(session, AUTHORIZED_USER_FILE, project=True)
+ session.env[EXPECT_PROJECT_ENV] = "1"
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_sync/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def compute_engine(session):
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ # unset Application Default Credentials so
+ # credentials are detected from environment
+ del session.virtualenv.env["GOOGLE_APPLICATION_CREDENTIALS"]
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_sync/test_compute_engine.py",
+ *session.posargs,
+ )
+
+
+@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)
+ session.install(*TEST_DEPENDENCIES_SYNC, "google-cloud-pubsub==1.7.0")
+ session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+ default(
+ session,
+ "system_tests_sync/test_grpc.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def requests(session):
+ session.install(LIBRARY_DIR)
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+ default(
+ session,
+ "system_tests_sync/test_requests.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def urllib3(session):
+ session.install(LIBRARY_DIR)
+ session.install(*TEST_DEPENDENCIES_SYNC)
+ session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+ default(
+ session,
+ "system_tests_sync/test_urllib3.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def mtls_http(session):
+ session.install(LIBRARY_DIR)
+ session.install(*TEST_DEPENDENCIES_SYNC, "pyopenssl")
+ session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+ default(
+ session,
+ "system_tests_sync/test_mtls_http.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def external_accounts(session):
+ session.install(
+ *TEST_DEPENDENCIES_SYNC,
+ LIBRARY_DIR,
+ "google-api-python-client",
+ )
+ default(
+ session,
+ "system_tests_sync/test_external_accounts.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_SYNC)
+def downscoping(session):
+ session.install(
+ *TEST_DEPENDENCIES_SYNC,
+ LIBRARY_DIR,
+ "google-cloud-storage",
+ )
+ default(
+ session,
+ "system_tests_sync/test_downscoping.py",
+ *session.posargs,
+ )
+
+
+# ASYNC SYSTEM TESTS
+
+@nox.session(python=PYTHON_VERSIONS_ASYNC)
+def service_account_async(session):
+ session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_async/test_service_account.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_ASYNC)
+def default_explicit_service_account_async(session):
+ session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+ session.env[EXPECT_PROJECT_ENV] = "1"
+ session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_async/test_default.py",
+ "system_tests_async/test_id_token.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_ASYNC)
+def default_explicit_authorized_user_async(session):
+ session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
+ session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_async/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_ASYNC)
+def default_explicit_authorized_user_explicit_project_async(session):
+ session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
+ session.env[EXPLICIT_PROJECT_ENV] = "example-project"
+ session.env[EXPECT_PROJECT_ENV] = "1"
+ session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_async/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_ASYNC)
+def default_cloud_sdk_service_account_async(session):
+ configure_cloud_sdk(session, SERVICE_ACCOUNT_FILE)
+ session.env[EXPECT_PROJECT_ENV] = "1"
+ session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_async/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_ASYNC)
+def default_cloud_sdk_authorized_user_async(session):
+ configure_cloud_sdk(session, AUTHORIZED_USER_FILE)
+ session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_async/test_default.py",
+ *session.posargs,
+ )
+
+
+@nox.session(python=PYTHON_VERSIONS_ASYNC)
+def default_cloud_sdk_authorized_user_configured_project_async(session):
+ configure_cloud_sdk(session, AUTHORIZED_USER_FILE, project=True)
+ session.env[EXPECT_PROJECT_ENV] = "1"
+ session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+ session.install(LIBRARY_DIR)
+ default(
+ session,
+ "system_tests_async/test_default.py",
+ *session.posargs,
+ )
diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc
new file mode 100644
index 0000000..5f20b1e
--- /dev/null
+++ b/system_tests/secrets.tar.enc
Binary files differ
diff --git a/system_tests/system_tests_async/__init__.py b/system_tests/system_tests_async/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/system_tests/system_tests_async/__init__.py
diff --git a/system_tests/system_tests_async/conftest.py b/system_tests/system_tests_async/conftest.py
new file mode 100644
index 0000000..9669099
--- /dev/null
+++ b/system_tests/system_tests_async/conftest.py
@@ -0,0 +1,115 @@
+# Copyright 2020 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 json
+import os
+
+from google.auth import _helpers
+import google.auth.transport.requests
+import google.auth.transport.urllib3
+import pytest
+import requests
+import urllib3
+
+import aiohttp
+from google.auth.transport import _aiohttp_requests as aiohttp_requests
+from system_tests.system_tests_sync import conftest as sync_conftest
+
+
+TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v3/tokeninfo"
+
+
+@pytest.fixture
+def service_account_file():
+ """The full path to a valid service account key file."""
+ yield sync_conftest.SERVICE_ACCOUNT_FILE
+
+
+@pytest.fixture
+def impersonated_service_account_file():
+ """The full path to a valid service account key file."""
+ yield sync_conftest.IMPERSONATED_SERVICE_ACCOUNT_FILE
+
+
+@pytest.fixture
+def authorized_user_file():
+ """The full path to a valid authorized user file."""
+ yield sync_conftest.AUTHORIZED_USER_FILE
+
+
+@pytest.fixture
+async def aiohttp_session():
+ async with aiohttp.ClientSession(auto_decompress=False) as session:
+ yield session
+
+
+@pytest.fixture(params=["aiohttp"])
+async def http_request(request, aiohttp_session):
+ """A transport.request object."""
+ yield aiohttp_requests.Request(aiohttp_session)
+
+
+@pytest.fixture
+async def token_info(http_request):
+ """Returns a function that obtains OAuth2 token info."""
+
+ async def _token_info(access_token=None, id_token=None):
+ query_params = {}
+
+ if access_token is not None:
+ query_params["access_token"] = access_token
+ elif id_token is not None:
+ query_params["id_token"] = id_token
+ else:
+ raise ValueError("No token specified.")
+
+ url = _helpers.update_query(sync_conftest.TOKEN_INFO_URL, query_params)
+
+ response = await http_request(url=url, method="GET")
+
+ data = await response.content()
+
+ return json.loads(data.decode("utf-8"))
+
+ yield _token_info
+
+
+@pytest.fixture
+async def verify_refresh(http_request):
+ """Returns a function that verifies that credentials can be refreshed."""
+
+ async def _verify_refresh(credentials):
+ if credentials.requires_scopes:
+ credentials = credentials.with_scopes(["email", "profile"])
+
+ await credentials.refresh(http_request)
+
+ assert credentials.token
+ assert credentials.valid
+
+ yield _verify_refresh
+
+
+def verify_environment():
+ """Checks to make sure that requisite data files are available."""
+ if not os.path.isdir(sync_conftest.DATA_DIR):
+ raise EnvironmentError(
+ "In order to run system tests, test data must exist in "
+ "system_tests/data. See CONTRIBUTING.rst for details."
+ )
+
+
+def pytest_configure(config):
+ """Pytest hook that runs before Pytest collects any tests."""
+ verify_environment()
diff --git a/system_tests/system_tests_async/test_default.py b/system_tests/system_tests_async/test_default.py
new file mode 100644
index 0000000..32299c0
--- /dev/null
+++ b/system_tests/system_tests_async/test_default.py
@@ -0,0 +1,29 @@
+# Copyright 2020 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
+import pytest
+
+from google.auth import _default_async
+
+EXPECT_PROJECT_ID = os.environ.get("EXPECT_PROJECT_ID")
+
+@pytest.mark.asyncio
+async def test_application_default_credentials(verify_refresh):
+ credentials, project_id = _default_async.default_async()
+
+ if EXPECT_PROJECT_ID is not None:
+ assert project_id is not None
+
+ await verify_refresh(credentials)
diff --git a/system_tests/system_tests_async/test_id_token.py b/system_tests/system_tests_async/test_id_token.py
new file mode 100644
index 0000000..a21b137
--- /dev/null
+++ b/system_tests/system_tests_async/test_id_token.py
@@ -0,0 +1,25 @@
+# Copyright 2020 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 pytest
+
+from google.auth import jwt
+import google.oauth2._id_token_async
+
+@pytest.mark.asyncio
+async def test_fetch_id_token(http_request):
+ audience = "https://pubsub.googleapis.com"
+ token = await google.oauth2._id_token_async.fetch_id_token(http_request, audience)
+
+ _, payload, _, _ = jwt._unverified_decode(token)
+ assert payload["aud"] == audience
diff --git a/system_tests/system_tests_async/test_service_account.py b/system_tests/system_tests_async/test_service_account.py
new file mode 100644
index 0000000..c1c16cc
--- /dev/null
+++ b/system_tests/system_tests_async/test_service_account.py
@@ -0,0 +1,53 @@
+# Copyright 2020 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 pytest
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import iam
+from google.oauth2 import _service_account_async
+
+
+@pytest.fixture
+def credentials(service_account_file):
+ yield _service_account_async.Credentials.from_service_account_file(service_account_file)
+
+
+@pytest.mark.asyncio
+async def test_refresh_no_scopes(http_request, credentials):
+ """
+ We expect the http request to refresh credentials
+ without scopes provided to throw an error.
+ """
+ with pytest.raises(exceptions.RefreshError):
+ await credentials.refresh(http_request)
+
+@pytest.mark.asyncio
+async def test_refresh_success(http_request, credentials, token_info):
+ credentials = credentials.with_scopes(["email", "profile"])
+ await credentials.refresh(http_request)
+
+ assert credentials.token
+
+ info = await token_info(credentials.token)
+
+ assert info["email"] == credentials.service_account_email
+ info_scopes = _helpers.string_to_scopes(info["scope"])
+ assert set(info_scopes) == set(
+ [
+ "https://www.googleapis.com/auth/userinfo.email",
+ "https://www.googleapis.com/auth/userinfo.profile",
+ ]
+ )
diff --git a/system_tests/system_tests_sync/.gitignore b/system_tests/system_tests_sync/.gitignore
new file mode 100644
index 0000000..be60550
--- /dev/null
+++ b/system_tests/system_tests_sync/.gitignore
@@ -0,0 +1,2 @@
+data
+secrets.tar \ No newline at end of file
diff --git a/system_tests/system_tests_sync/__init__.py b/system_tests/system_tests_sync/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/system_tests/system_tests_sync/__init__.py
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/conftest.py b/system_tests/system_tests_sync/conftest.py
new file mode 100644
index 0000000..16caa65
--- /dev/null
+++ b/system_tests/system_tests_sync/conftest.py
@@ -0,0 +1,141 @@
+# 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 json
+import os
+
+from google.auth import _helpers
+import google.auth.transport.requests
+import google.auth.transport.urllib3
+import pytest
+import requests
+import urllib3
+
+
+HERE = os.path.dirname(__file__)
+DATA_DIR = os.path.join(HERE, "../data")
+IMPERSONATED_SERVICE_ACCOUNT_FILE = os.path.join(
+ DATA_DIR, "impersonated_service_account.json"
+)
+SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
+URLLIB3_HTTP = urllib3.PoolManager(retries=False)
+REQUESTS_SESSION = requests.Session()
+REQUESTS_SESSION.verify = False
+TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v3/tokeninfo"
+
+
+@pytest.fixture
+def service_account_file():
+ """The full path to a valid service account key file."""
+ yield SERVICE_ACCOUNT_FILE
+
+
+@pytest.fixture
+def impersonated_service_account_file():
+ """The full path to a valid service account key file."""
+ yield IMPERSONATED_SERVICE_ACCOUNT_FILE
+
+
+@pytest.fixture
+def authorized_user_file():
+ """The full path to a valid authorized user file."""
+ yield AUTHORIZED_USER_FILE
+
+
+@pytest.fixture(params=["urllib3", "requests"])
+def request_type(request):
+ yield request.param
+
+
+@pytest.fixture
+def http_request(request_type):
+ """A transport.request object."""
+ if request_type == "urllib3":
+ yield google.auth.transport.urllib3.Request(URLLIB3_HTTP)
+ elif request_type == "requests":
+ yield google.auth.transport.requests.Request(REQUESTS_SESSION)
+
+
+@pytest.fixture
+def authenticated_request(request_type):
+ """A transport.request object that takes credentials"""
+ if request_type == "urllib3":
+
+ def wrapper(credentials):
+ return google.auth.transport.urllib3.AuthorizedHttp(
+ credentials, http=URLLIB3_HTTP
+ ).request
+
+ yield wrapper
+ elif request_type == "requests":
+
+ def wrapper(credentials):
+ session = google.auth.transport.requests.AuthorizedSession(credentials)
+ session.verify = False
+ return google.auth.transport.requests.Request(session)
+
+ yield wrapper
+
+
+@pytest.fixture
+def token_info(http_request):
+ """Returns a function that obtains OAuth2 token info."""
+
+ def _token_info(access_token=None, id_token=None):
+ query_params = {}
+
+ if access_token is not None:
+ query_params["access_token"] = access_token
+ elif id_token is not None:
+ query_params["id_token"] = id_token
+ else:
+ raise ValueError("No token specified.")
+
+ url = _helpers.update_query(TOKEN_INFO_URL, query_params)
+
+ response = http_request(url=url, method="GET")
+
+ return json.loads(response.data.decode("utf-8"))
+
+ yield _token_info
+
+
+@pytest.fixture
+def verify_refresh(http_request):
+ """Returns a function that verifies that credentials can be refreshed."""
+
+ def _verify_refresh(credentials):
+ if credentials.requires_scopes:
+ credentials = credentials.with_scopes(["email", "profile"])
+
+ credentials.refresh(http_request)
+
+ assert credentials.token
+ assert credentials.valid
+
+ yield _verify_refresh
+
+
+def verify_environment():
+ """Checks to make sure that requisite data files are available."""
+ if not os.path.isdir(DATA_DIR):
+ raise EnvironmentError(
+ "In order to run system tests, test data must exist in "
+ "system_tests/data. See CONTRIBUTING.rst for details."
+ )
+
+
+def pytest_configure(config):
+ """Pytest hook that runs before Pytest collects any tests."""
+ verify_environment()
diff --git a/system_tests/system_tests_sync/secrets.tar.enc b/system_tests/system_tests_sync/secrets.tar.enc
new file mode 100644
index 0000000..29e0692
--- /dev/null
+++ b/system_tests/system_tests_sync/secrets.tar.enc
Binary files differ
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_compute_engine.py b/system_tests/system_tests_sync/test_compute_engine.py
new file mode 100644
index 0000000..1e0eaf1
--- /dev/null
+++ b/system_tests/system_tests_sync/test_compute_engine.py
@@ -0,0 +1,75 @@
+# 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 datetime import datetime
+
+import pytest
+
+import google.auth
+from google.auth import compute_engine
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth.compute_engine import _metadata
+import google.oauth2.id_token
+
+AUDIENCE = "https://pubsub.googleapis.com"
+
+
+@pytest.fixture(autouse=True)
+def check_gce_environment(http_request):
+ try:
+ _metadata.get_service_account_info(http_request)
+ except exceptions.TransportError:
+ pytest.skip("Compute Engine metadata service is not available.")
+
+
+def test_refresh(http_request, token_info):
+ credentials = compute_engine.Credentials()
+
+ credentials.refresh(http_request)
+
+ assert credentials.token is not None
+ assert credentials.service_account_email is not None
+
+ info = token_info(credentials.token)
+ info_scopes = _helpers.string_to_scopes(info["scope"])
+ assert set(info_scopes) == set(credentials.scopes)
+
+
+def test_default(verify_refresh):
+ credentials, project_id = google.auth.default()
+
+ assert project_id is not None
+ assert isinstance(credentials, compute_engine.Credentials)
+ verify_refresh(credentials)
+
+
+def test_id_token_from_metadata(http_request):
+ credentials = compute_engine.IDTokenCredentials(
+ http_request, AUDIENCE, use_metadata_identity_endpoint=True
+ )
+ credentials.refresh(http_request)
+
+ _, payload, _, _ = jwt._unverified_decode(credentials.token)
+ assert credentials.valid
+ assert payload["aud"] == AUDIENCE
+ assert datetime.fromtimestamp(payload["exp"]) == credentials.expiry
+
+
+def test_fetch_id_token(http_request):
+ token = google.oauth2.id_token.fetch_id_token(http_request, AUDIENCE)
+
+ _, payload, _, _ = jwt._unverified_decode(token)
+ assert payload["aud"] == AUDIENCE
diff --git a/system_tests/system_tests_sync/test_default.py b/system_tests/system_tests_sync/test_default.py
new file mode 100644
index 0000000..560ab32
--- /dev/null
+++ b/system_tests/system_tests_sync/test_default.py
@@ -0,0 +1,28 @@
+# 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
+
+import google.auth
+
+EXPECT_PROJECT_ID = os.environ.get("EXPECT_PROJECT_ID")
+
+
+def test_application_default_credentials(verify_refresh):
+ credentials, project_id = google.auth.default()
+
+ if EXPECT_PROJECT_ID is not None:
+ assert project_id is not None
+
+ verify_refresh(credentials)
diff --git a/system_tests/system_tests_sync/test_downscoping.py b/system_tests/system_tests_sync/test_downscoping.py
new file mode 100644
index 0000000..fdb4efa
--- /dev/null
+++ b/system_tests/system_tests_sync/test_downscoping.py
@@ -0,0 +1,162 @@
+# Copyright 2021 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 re
+import uuid
+
+import google.auth
+
+from google.auth import downscoped
+from google.auth.transport import requests
+from google.cloud import exceptions
+from google.cloud import storage
+from google.oauth2 import credentials
+
+import pytest
+
+ # 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 = "{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.
+_INACCESSIBLE_CONTENT = "secret content"
+# The object name of the object inaccessible by the downscoped token.
+_INACCESSIBLE_OBJECT_NAME = "other-customer-data.txt"
+
+
+@pytest.fixture(scope="module")
+def temp_bucket():
+ """Yields a bucket that is deleted after the test completes."""
+ bucket = None
+ while bucket is None or bucket.exists():
+ bucket_name = "auth-python-downscope-test-{}".format(uuid.uuid4())
+ bucket = storage.Client().bucket(bucket_name)
+ bucket = storage.Client().create_bucket(bucket.name)
+ yield bucket
+ bucket.delete(force=True)
+
+
+@pytest.fixture(scope="module")
+def temp_blobs(temp_bucket):
+ """Yields two blobs that are deleted after the test completes."""
+ bucket = temp_bucket
+ # Downscoped tokens will have readonly access to this blob.
+ accessible_blob = bucket.blob(_ACCESSIBLE_OBJECT_NAME)
+ accessible_blob.upload_from_string(_ACCESSIBLE_CONTENT)
+ # Downscoped tokens will have no access to this blob.
+ inaccessible_blob = bucket.blob(_INACCESSIBLE_OBJECT_NAME)
+ inaccessible_blob.upload_from_string(_INACCESSIBLE_CONTENT)
+ yield (accessible_blob, inaccessible_blob)
+ bucket.delete_blobs([accessible_blob, inaccessible_blob])
+
+
+def get_token_from_broker(bucket_name, object_prefix):
+ """Simulates token broker generating downscoped tokens for specified bucket.
+
+ Args:
+ bucket_name (str): The name of the Cloud Storage bucket.
+ object_prefix (str): The prefix string of the object name. This is used
+ to ensure access is restricted to only objects starting with this
+ prefix string.
+
+ Returns:
+ Tuple[str, datetime.datetime]: The downscoped access token and its expiry date.
+ """
+ # Initialize the Credential Access Boundary rules.
+ 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 = (
+ "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.
+ rule = downscoped.AccessBoundaryRule(
+ available_resource=available_resource,
+ available_permissions=available_permissions,
+ availability_condition=availability_condition,
+ )
+ # Define the Credential Access Boundary with all the relevant rules.
+ credential_access_boundary = downscoped.CredentialAccessBoundary(rules=[rule])
+
+ # Retrieve the source credentials via ADC.
+ source_credentials, _ = google.auth.default()
+ if source_credentials.requires_scopes:
+ source_credentials = source_credentials.with_scopes(
+ ["https://www.googleapis.com/auth/cloud-platform"]
+ )
+
+ # Create the downscoped credentials.
+ downscoped_credentials = downscoped.Credentials(
+ source_credentials=source_credentials,
+ credential_access_boundary=credential_access_boundary,
+ )
+
+ # Refresh the tokens.
+ downscoped_credentials.refresh(requests.Request())
+
+ # These values will need to be passed to the token consumer.
+ access_token = downscoped_credentials.token
+ expiry = downscoped_credentials.expiry
+ return (access_token, expiry)
+
+
+def test_downscoping(temp_blobs):
+ """Tests token consumer access to cloud storage using downscoped tokens.
+
+ Args:
+ temp_blobs (Tuple[google.cloud.storage.blob.Blob, ...]): The temporarily
+ created test cloud storage blobs (one readonly accessible, the other
+ not).
+ """
+ accessible_blob, inaccessible_blob = temp_blobs
+ bucket_name = accessible_blob.bucket.name
+ # Create the OAuth credentials from the downscoped token and pass a
+ # refresh handler to handle token expiration. We are passing a
+ # refresh_handler instead of a one-time access token/expiry pair.
+ # This will allow testing this on-demand method for getting access tokens.
+ def refresh_handler(request, scopes=None):
+ # Get readonly access tokens to objects with accessible prefix in
+ # the temporarily created bucket.
+ return get_token_from_broker(bucket_name, _OBJECT_PREFIX)
+
+ creds = credentials.Credentials(
+ None,
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
+ refresh_handler=refresh_handler,
+ )
+
+ # Initialize a Cloud Storage client with the oauth2 credentials.
+ storage_client = storage.Client(credentials=creds)
+
+ # Test read access succeeds to accessible blob.
+ bucket = storage_client.bucket(bucket_name)
+ blob = bucket.blob(accessible_blob.name)
+ assert blob.download_as_bytes().decode("utf-8") == _ACCESSIBLE_CONTENT
+
+ # Test write access fails.
+ with pytest.raises(exceptions.Forbidden) as excinfo:
+ blob.upload_from_string("Write operations are not allowed")
+
+ assert excinfo.match(r"does not have storage.objects.create access")
+
+ # Test read access fails to inaccessible blob.
+ with pytest.raises(exceptions.Forbidden) as excinfo:
+ bucket.blob(inaccessible_blob.name).download_as_bytes()
+
+ assert excinfo.match(r"does not have storage.objects.get access")
diff --git a/system_tests/system_tests_sync/test_external_accounts.py b/system_tests/system_tests_sync/test_external_accounts.py
new file mode 100644
index 0000000..e24c7b4
--- /dev/null
+++ b/system_tests/system_tests_sync/test_external_accounts.py
@@ -0,0 +1,305 @@
+# Copyright 2021 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.
+
+# Prerequisites:
+# Make sure to run the setup in scripts/setup_external_accounts.sh
+# and copy the logged constant strings (_AUDIENCE_OIDC, _AUDIENCE_AWS)
+# into this file before running this test suite.
+# Once that is done, this test can be run indefinitely.
+#
+# The only requirement for this test suite to run is to set the environment
+# variable GOOGLE_APPLICATION_CREDENTIALS to point to the expected service
+# account keys whose email is referred to in the setup script.
+#
+# This script follows the following logic.
+# OIDC provider (file-sourced and url-sourced credentials):
+# Use the service account keys to generate a Google ID token using the
+# iamcredentials generateIdToken API, using the default STS audience.
+# This will use the service account client ID as the sub field of the token.
+# This OIDC token will be used as the external subject token to be exchanged
+# for a Google access token via GCP STS endpoint and then to impersonate the
+# original service account key.
+
+
+import json
+import os
+import socket
+from tempfile import NamedTemporaryFile
+import threading
+
+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"
+_AUDIENCE_AWS = "//iam.googleapis.com/projects/79992041559/locations/global/workloadIdentityPools/pool-73wslmxn/providers/aws-73wslmxn"
+_ROLE_AWS = "arn:aws:iam::077071391996:role/ci-python-test"
+
+
+def dns_access_direct(request, project_id):
+ # First, get the default credentials.
+ credentials, _ = google.auth.default(
+ scopes=["https://www.googleapis.com/auth/cloud-platform.read-only"],
+ request=request,
+ )
+
+ # Apply the default credentials to the headers to make the request.
+ headers = {}
+ credentials.apply(headers)
+ response = request(
+ url="https://dns.googleapis.com/dns/v1/projects/{}".format(project_id),
+ headers=headers,
+ )
+
+ if response.status == 200:
+ return response.data
+
+
+def dns_access_client_library(_, project_id):
+ service = discovery.build("dns", "v1")
+ request = service.projects().get(project=project_id)
+ return request.execute()
+
+
+@pytest.fixture(params=[dns_access_direct, dns_access_client_library])
+def dns_access(request, http_request, service_account_info):
+ # Fill in the fixtures on the functions,
+ # so that we don't have to fill in the parameters manually.
+ def wrapper():
+ return request.param(http_request, service_account_info["project_id"])
+
+ yield wrapper
+
+
+@pytest.fixture
+def oidc_credentials(service_account_file, http_request):
+ result = service_account.IDTokenCredentials.from_service_account_file(
+ service_account_file, target_audience=_AUDIENCE_OIDC
+ )
+ result.refresh(http_request)
+ yield result
+
+
+@pytest.fixture
+def service_account_info(service_account_file):
+ with open(service_account_file) as f:
+ yield json.load(f)
+
+
+@pytest.fixture
+def aws_oidc_credentials(
+ service_account_file, service_account_info, authenticated_request
+):
+ credentials = service_account.Credentials.from_service_account_file(
+ service_account_file, scopes=["https://www.googleapis.com/auth/cloud-platform"]
+ )
+ result = authenticated_request(credentials)(
+ url="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateIdToken".format(
+ service_account_info["client_email"]
+ ),
+ method="POST",
+ body=json.dumps(
+ {"audience": service_account_info["client_id"], "includeEmail": True}
+ ),
+ )
+ assert result.status == 200
+
+ yield json.loads(result.data)["token"]
+
+
+# Our external accounts tests involve setting up some preconditions, setting a
+# credential file, and then making sure that our client libraries can work with
+# the set credentials.
+def get_project_dns(dns_access, credential_data):
+ with NamedTemporaryFile() as credfile:
+ credfile.write(json.dumps(credential_data).encode("utf-8"))
+ credfile.flush()
+ old_credentials = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
+
+ with patch.dict(os.environ, {"GOOGLE_APPLICATION_CREDENTIALS": credfile.name}):
+ # If our setup and credential file are correct,
+ # discovery.build should be able to establish these as the default credentials.
+ return dns_access()
+
+
+def get_xml_value_by_tagname(data, tagname):
+ startIndex = data.index("<{}>".format(tagname))
+ if startIndex >= 0:
+ endIndex = data.index("</{}>".format(tagname), startIndex)
+ if endIndex > startIndex:
+ return data[startIndex + len(tagname) + 2 : endIndex]
+
+
+# This test makes sure that setting an accesible credential file
+# works to allow access to Google resources.
+def test_file_based_external_account(
+ oidc_credentials, service_account_info, dns_access
+):
+ with NamedTemporaryFile() as tmpfile:
+ tmpfile.write(oidc_credentials.token.encode("utf-8"))
+ tmpfile.flush()
+
+ assert get_project_dns(
+ dns_access,
+ {
+ "type": "external_account",
+ "audience": _AUDIENCE_OIDC,
+ "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+ "token_url": "https://sts.googleapis.com/v1/token",
+ "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+ oidc_credentials.service_account_email
+ ),
+ "credential_source": {
+ "file": tmpfile.name,
+ },
+ },
+ )
+
+
+# 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(BaseHTTPServer.BaseHTTPRequestHandler):
+ def do_GET(self):
+ if self.headers["my-header"] != "expected-value":
+ self.send_response(400)
+ self.send_header("Content-Type", "application/json")
+ self.end_headers()
+ self.wfile.write(
+ json.dumps({"error": "missing header"}).encode("utf-8")
+ )
+ elif self.path != "/token":
+ self.send_response(400)
+ self.send_header("Content-Type", "application/json")
+ self.end_headers()
+ self.wfile.write(
+ json.dumps({"error": "incorrect token path"}).encode("utf-8")
+ )
+ else:
+ self.send_response(200)
+ self.send_header("Content-Type", "application/json")
+ self.end_headers()
+ self.wfile.write(
+ json.dumps({"access_token": oidc_credentials.token}).encode("utf-8")
+ )
+
+ class TestHTTPServer(BaseHTTPServer.HTTPServer, object):
+ def __init__(self):
+ self.port = self._find_open_port()
+ super(TestHTTPServer, self).__init__(("", self.port), TestResponseHandler)
+
+ @staticmethod
+ def _find_open_port():
+ s = socket.socket()
+ s.bind(("", 0))
+ return s.getsockname()[1]
+
+ # This makes sure that the server gets shut down when this variable leaves its "with" block
+ # The python3 HttpServer has __enter__ and __exit__ methods, but python2 does not.
+ # By redefining the __enter__ and __exit__ methods, we ensure that python2 and python3 act similarly
+ def __exit__(self, *args):
+ self.shutdown()
+
+ def __enter__(self):
+ return self
+
+ with TestHTTPServer() as server:
+ threading.Thread(target=server.serve_forever).start()
+
+ assert get_project_dns(
+ dns_access,
+ {
+ "type": "external_account",
+ "audience": _AUDIENCE_OIDC,
+ "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+ "token_url": "https://sts.googleapis.com/v1/token",
+ "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+ oidc_credentials.service_account_email
+ ),
+ "credential_source": {
+ "url": "http://localhost:{}/token".format(server.port),
+ "headers": {"my-header": "expected-value"},
+ "format": {
+ "type": "json",
+ "subject_token_field_name": "access_token",
+ },
+ },
+ },
+ )
+
+
+# AWS provider tests for AWS credentials
+# The test suite will also run tests for AWS credentials. This works as
+# follows. (Note prequisite setup is needed. This is documented in
+# setup_external_accounts.sh).
+# - iamcredentials:generateIdToken is used to generate a Google ID token using
+# the service account access token. The service account client_id is used as
+# audience.
+# - AWS STS AssumeRoleWithWebIdentity API is used to exchange this token for
+# temporary AWS security credentials for a specified AWS ARN role.
+# - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN
+# environment variables are set using these credentials before the test is
+# run simulating an AWS VM.
+# - The test can now be run.
+def test_aws_based_external_account(
+ aws_oidc_credentials, service_account_info, dns_access, http_request
+):
+
+ response = http_request(
+ url=(
+ "https://sts.amazonaws.com/"
+ "?Action=AssumeRoleWithWebIdentity"
+ "&Version=2011-06-15"
+ "&DurationSeconds=3600"
+ "&RoleSessionName=python-test"
+ "&RoleArn={}"
+ "&WebIdentityToken={}"
+ ).format(_ROLE_AWS, aws_oidc_credentials)
+ )
+ assert response.status == 200
+
+ # The returned data is in XML, but loading an XML parser would be overkill.
+ # Searching the return text manually for the start and finish tag.
+ data = response.data.decode("utf-8")
+
+ with patch.dict(
+ os.environ,
+ {
+ "AWS_REGION": "us-east-2",
+ "AWS_ACCESS_KEY_ID": get_xml_value_by_tagname(data, "AccessKeyId"),
+ "AWS_SECRET_ACCESS_KEY": get_xml_value_by_tagname(data, "SecretAccessKey"),
+ "AWS_SESSION_TOKEN": get_xml_value_by_tagname(data, "SessionToken"),
+ },
+ ):
+ assert get_project_dns(
+ dns_access,
+ {
+ "type": "external_account",
+ "audience": _AUDIENCE_AWS,
+ "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
+ "token_url": "https://sts.googleapis.com/v1/token",
+ "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+ service_account_info["client_email"]
+ ),
+ "credential_source": {
+ "environment_id": "aws1",
+ "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+ },
+ },
+ )
diff --git a/system_tests/system_tests_sync/test_grpc.py b/system_tests/system_tests_sync/test_grpc.py
new file mode 100644
index 0000000..7f548ec
--- /dev/null
+++ b/system_tests/system_tests_sync/test_grpc.py
@@ -0,0 +1,93 @@
+# 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 google.auth
+import google.auth.credentials
+import google.auth.jwt
+import google.auth.transport.grpc
+from google.oauth2 import service_account
+
+from google.cloud import pubsub_v1
+
+
+def test_grpc_request_with_regular_credentials(http_request):
+ credentials, project_id = google.auth.default()
+ credentials = google.auth.credentials.with_scopes_if_required(
+ credentials, scopes=["https://www.googleapis.com/auth/pubsub"]
+ )
+
+
+ # Create a pub/sub client.
+ client = pubsub_v1.PublisherClient(credentials=credentials)
+
+ # list the topics and drain the iterator to test that an authorized API
+ # call works.
+ list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
+ list(list_topics_iter)
+
+
+def test_grpc_request_with_regular_credentials_and_self_signed_jwt(http_request):
+ credentials, project_id = google.auth.default()
+
+ # At the time this test is being written, there are no GAPIC libraries
+ # that will trigger the self-signed JWT flow. Manually create the self-signed
+ # jwt on the service account credential to check that the request
+ # succeeds.
+ credentials = credentials.with_scopes(
+ scopes=[], default_scopes=["https://www.googleapis.com/auth/pubsub"]
+ )
+ credentials._create_self_signed_jwt(audience="https://pubsub.googleapis.com/")
+
+ # Create a pub/sub client.
+ client = pubsub_v1.PublisherClient(credentials=credentials)
+
+ # list the topics and drain the iterator to test that an authorized API
+ # call works.
+ list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
+ list(list_topics_iter)
+
+ # Check that self-signed JWT was created and is being used
+ assert credentials._jwt_credentials is not None
+ assert credentials._jwt_credentials.token == credentials.token
+
+
+def test_grpc_request_with_jwt_credentials():
+ credentials, project_id = google.auth.default()
+ audience = "https://pubsub.googleapis.com/google.pubsub.v1.Publisher"
+ credentials = google.auth.jwt.Credentials.from_signing_credentials(
+ credentials, audience=audience
+ )
+
+ # Create a pub/sub client.
+ client = pubsub_v1.PublisherClient(credentials=credentials)
+
+ # list the topics and drain the iterator to test that an authorized API
+ # call works.
+ list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
+ list(list_topics_iter)
+
+
+def test_grpc_request_with_on_demand_jwt_credentials():
+ credentials, project_id = google.auth.default()
+ credentials = google.auth.jwt.OnDemandCredentials.from_signing_credentials(
+ credentials
+ )
+
+ # Create a pub/sub client.
+ client = pubsub_v1.PublisherClient(credentials=credentials)
+
+ # list the topics and drain the iterator to test that an authorized API
+ # call works.
+ list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
+ list(list_topics_iter)
diff --git a/system_tests/system_tests_sync/test_id_token.py b/system_tests/system_tests_sync/test_id_token.py
new file mode 100644
index 0000000..b07cefc
--- /dev/null
+++ b/system_tests/system_tests_sync/test_id_token.py
@@ -0,0 +1,25 @@
+# Copyright 2020 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 pytest
+
+from google.auth import jwt
+import google.oauth2.id_token
+
+
+def test_fetch_id_token(http_request):
+ audience = "https://pubsub.googleapis.com"
+ token = google.oauth2.id_token.fetch_id_token(http_request, audience)
+
+ _, payload, _, _ = jwt._unverified_decode(token)
+ assert payload["aud"] == audience
diff --git a/system_tests/system_tests_sync/test_impersonated_credentials.py b/system_tests/system_tests_sync/test_impersonated_credentials.py
new file mode 100644
index 0000000..6689e89
--- /dev/null
+++ b/system_tests/system_tests_sync/test_impersonated_credentials.py
@@ -0,0 +1,99 @@
+# Copyright 2020 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 json
+import pytest
+
+import google.oauth2.credentials
+from google.oauth2 import service_account
+import google.auth.impersonated_credentials
+from google.auth import _helpers
+
+
+GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
+
+
+@pytest.fixture
+def service_account_credentials(service_account_file):
+ yield service_account.Credentials.from_service_account_file(service_account_file)
+
+
+@pytest.fixture
+def impersonated_service_account_credentials(impersonated_service_account_file):
+ yield service_account.Credentials.from_service_account_file(
+ impersonated_service_account_file
+ )
+
+
+def test_refresh_with_user_credentials_as_source(
+ authorized_user_file,
+ impersonated_service_account_credentials,
+ http_request,
+ token_info,
+):
+ with open(authorized_user_file, "r") as fh:
+ info = json.load(fh)
+
+ source_credentials = google.oauth2.credentials.Credentials(
+ None,
+ refresh_token=info["refresh_token"],
+ token_uri=GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+ client_id=info["client_id"],
+ client_secret=info["client_secret"],
+ # The source credential needs this scope for the generateAccessToken request
+ # The user must also have `Service Account Token Creator` on the project
+ # that owns the impersonated service account.
+ # See https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
+ )
+
+ source_credentials.refresh(http_request)
+
+ target_scopes = [
+ "https://www.googleapis.com/auth/devstorage.read_only",
+ "https://www.googleapis.com/auth/analytics",
+ ]
+ target_credentials = google.auth.impersonated_credentials.Credentials(
+ source_credentials=source_credentials,
+ target_principal=impersonated_service_account_credentials.service_account_email,
+ target_scopes=target_scopes,
+ lifetime=100,
+ )
+
+ target_credentials.refresh(http_request)
+ assert target_credentials.token
+
+
+def test_refresh_with_service_account_credentials_as_source(
+ http_request,
+ service_account_credentials,
+ impersonated_service_account_credentials,
+ token_info,
+):
+ source_credentials = service_account_credentials.with_scopes(["email"])
+ source_credentials.refresh(http_request)
+ assert source_credentials.token
+
+ target_scopes = [
+ "https://www.googleapis.com/auth/devstorage.read_only",
+ "https://www.googleapis.com/auth/analytics",
+ ]
+ target_credentials = google.auth.impersonated_credentials.Credentials(
+ source_credentials=source_credentials,
+ target_principal=impersonated_service_account_credentials.service_account_email,
+ target_scopes=target_scopes,
+ )
+
+ target_credentials.refresh(http_request)
+ assert target_credentials.token
diff --git a/system_tests/system_tests_sync/test_mtls_http.py b/system_tests/system_tests_sync/test_mtls_http.py
new file mode 100644
index 0000000..bcf2a59
--- /dev/null
+++ b/system_tests/system_tests_sync/test_mtls_http.py
@@ -0,0 +1,124 @@
+# Copyright 2020 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 json
+import mock
+import os
+import time
+from os import path
+
+
+import google.auth
+import google.auth.credentials
+from google.auth import environment_vars
+from google.auth.transport import mtls
+import google.auth.transport.requests
+import google.auth.transport.urllib3
+
+MTLS_ENDPOINT = "https://pubsub.mtls.googleapis.com/v1/projects/{}/topics"
+REGULAR_ENDPOINT = "https://pubsub.googleapis.com/v1/projects/{}/topics"
+
+
+def test_requests():
+ credentials, project_id = google.auth.default()
+ credentials = google.auth.credentials.with_scopes_if_required(
+ credentials, ["https://www.googleapis.com/auth/pubsub"]
+ )
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+ with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
+ authed_session.configure_mtls_channel()
+
+ # If the devices has default client cert source, then a mutual TLS channel
+ # is supposed to be created.
+ assert authed_session.is_mtls == mtls.has_default_client_cert_source()
+
+ # Sleep 1 second to avoid 503 error.
+ time.sleep(1)
+
+ if authed_session.is_mtls:
+ response = authed_session.get(MTLS_ENDPOINT.format(project_id))
+ else:
+ response = authed_session.get(REGULAR_ENDPOINT.format(project_id))
+
+ assert response.ok
+
+
+def test_urllib3():
+ credentials, project_id = google.auth.default()
+ credentials = google.auth.credentials.with_scopes_if_required(
+ credentials, ["https://www.googleapis.com/auth/pubsub"]
+ )
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
+ with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
+ is_mtls = authed_http.configure_mtls_channel()
+
+ # If the devices has default client cert source, then a mutual TLS channel
+ # is supposed to be created.
+ assert is_mtls == mtls.has_default_client_cert_source()
+
+ # Sleep 1 second to avoid 503 error.
+ time.sleep(1)
+
+ if is_mtls:
+ response = authed_http.request("GET", MTLS_ENDPOINT.format(project_id))
+ else:
+ response = authed_http.request("GET", REGULAR_ENDPOINT.format(project_id))
+
+ assert response.status == 200
+
+
+def test_requests_with_default_client_cert_source():
+ credentials, project_id = google.auth.default()
+ credentials = google.auth.credentials.with_scopes_if_required(
+ credentials, ["https://www.googleapis.com/auth/pubsub"]
+ )
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+
+ if mtls.has_default_client_cert_source():
+ with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
+ authed_session.configure_mtls_channel(
+ client_cert_callback=mtls.default_client_cert_source()
+ )
+
+ assert authed_session.is_mtls
+
+ # Sleep 1 second to avoid 503 error.
+ time.sleep(1)
+
+ response = authed_session.get(MTLS_ENDPOINT.format(project_id))
+ assert response.ok
+
+
+def test_urllib3_with_default_client_cert_source():
+ credentials, project_id = google.auth.default()
+ credentials = google.auth.credentials.with_scopes_if_required(
+ credentials, ["https://www.googleapis.com/auth/pubsub"]
+ )
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
+
+ if mtls.has_default_client_cert_source():
+ with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
+ assert authed_http.configure_mtls_channel(
+ client_cert_callback=mtls.default_client_cert_source()
+ )
+
+ # Sleep 1 second to avoid 503 error.
+ time.sleep(1)
+
+ response = authed_http.request("GET", MTLS_ENDPOINT.format(project_id))
+ assert response.status == 200
diff --git a/system_tests/system_tests_sync/test_oauth2_credentials.py b/system_tests/system_tests_sync/test_oauth2_credentials.py
new file mode 100644
index 0000000..908db31
--- /dev/null
+++ b/system_tests/system_tests_sync/test_oauth2_credentials.py
@@ -0,0 +1,55 @@
+# 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 json
+
+from google.auth import _helpers
+import google.oauth2.credentials
+
+GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
+
+
+def test_refresh(authorized_user_file, http_request, token_info):
+ with open(authorized_user_file, "r") as fh:
+ info = json.load(fh)
+
+ credentials = google.oauth2.credentials.Credentials(
+ None, # No access token, must be refreshed.
+ refresh_token=info["refresh_token"],
+ token_uri=GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+ client_id=info["client_id"],
+ client_secret=info["client_secret"],
+ )
+
+ credentials.refresh(http_request)
+
+ assert credentials.token
+
+ info = token_info(credentials.token)
+
+ info_scopes = _helpers.string_to_scopes(info["scope"])
+
+ # Canonical list of scopes at https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
+ # or do `gcloud auth application-defaut login --help`
+ canonical_scopes = set(
+ [
+ "https://www.googleapis.com/auth/userinfo.email",
+ "https://www.googleapis.com/auth/cloud-platform",
+ "openid",
+ ]
+ )
+ # When running the test locally, we always have an additional "accounts.reauth" scope.
+ canonical_scopes_with_reauth = canonical_scopes.copy()
+ canonical_scopes_with_reauth.add("https://www.googleapis.com/auth/accounts.reauth")
+ assert set(info_scopes) == canonical_scopes or set(info_scopes) == canonical_scopes_with_reauth
diff --git a/system_tests/system_tests_sync/test_requests.py b/system_tests/system_tests_sync/test_requests.py
new file mode 100644
index 0000000..2800484
--- /dev/null
+++ b/system_tests/system_tests_sync/test_requests.py
@@ -0,0 +1,42 @@
+# Copyright 2021 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 google.auth
+import google.auth.credentials
+import google.auth.transport.requests
+from google.oauth2 import service_account
+
+
+def test_authorized_session_with_service_account_and_self_signed_jwt():
+ credentials, project_id = google.auth.default()
+
+ credentials = credentials.with_scopes(
+ scopes=[],
+ default_scopes=["https://www.googleapis.com/auth/pubsub"],
+ )
+
+ session = google.auth.transport.requests.AuthorizedSession(
+ credentials=credentials, default_host="pubsub.googleapis.com"
+ )
+
+ # List Pub/Sub Topics through the REST API
+ # https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/list
+ url = "https://pubsub.googleapis.com/v1/projects/{}/topics".format(project_id)
+ with session:
+ response = session.get(url)
+ response.raise_for_status()
+
+ # Check that self-signed JWT was created and is being used
+ assert credentials._jwt_credentials is not None
+ assert credentials._jwt_credentials.token == credentials.token
diff --git a/system_tests/system_tests_sync/test_service_account.py b/system_tests/system_tests_sync/test_service_account.py
new file mode 100644
index 0000000..498b75b
--- /dev/null
+++ b/system_tests/system_tests_sync/test_service_account.py
@@ -0,0 +1,65 @@
+# 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 pytest
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import iam
+from google.oauth2 import service_account
+
+
+@pytest.fixture
+def credentials(service_account_file):
+ yield service_account.Credentials.from_service_account_file(service_account_file)
+
+
+def test_refresh_no_scopes(http_request, credentials):
+ with pytest.raises(exceptions.RefreshError):
+ credentials.refresh(http_request)
+
+
+def test_refresh_success(http_request, credentials, token_info):
+ credentials = credentials.with_scopes(["email", "profile"])
+
+ credentials.refresh(http_request)
+
+ assert credentials.token
+
+ info = token_info(credentials.token)
+
+ assert info["email"] == credentials.service_account_email
+ info_scopes = _helpers.string_to_scopes(info["scope"])
+ assert set(info_scopes) == set(
+ [
+ "https://www.googleapis.com/auth/userinfo.email",
+ "https://www.googleapis.com/auth/userinfo.profile",
+ ]
+ )
+
+def test_iam_signer(http_request, credentials):
+ credentials = credentials.with_scopes(
+ ["https://www.googleapis.com/auth/iam"]
+ )
+
+ # Verify iamcredentials signer.
+ signer = iam.Signer(
+ http_request,
+ credentials,
+ credentials.service_account_email
+ )
+
+ signed_blob = signer.sign("message")
+
+ assert isinstance(signed_blob, bytes)
diff --git a/system_tests/system_tests_sync/test_urllib3.py b/system_tests/system_tests_sync/test_urllib3.py
new file mode 100644
index 0000000..1932e19
--- /dev/null
+++ b/system_tests/system_tests_sync/test_urllib3.py
@@ -0,0 +1,44 @@
+# Copyright 2021 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 google.auth
+import google.auth.credentials
+import google.auth.transport.requests
+from google.oauth2 import service_account
+
+
+def test_authorized_session_with_service_account_and_self_signed_jwt():
+ credentials, project_id = google.auth.default()
+
+ credentials = credentials.with_scopes(
+ scopes=[],
+ default_scopes=["https://www.googleapis.com/auth/pubsub"],
+ )
+
+ http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials=credentials, default_host="pubsub.googleapis.com"
+ )
+
+ # List Pub/Sub Topics through the REST API
+ # https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/list
+ response = http.urlopen(
+ method="GET",
+ url="https://pubsub.googleapis.com/v1/projects/{}/topics".format(project_id)
+ )
+
+ assert response.status == 200
+
+ # Check that self-signed JWT was created and is being used
+ assert credentials._jwt_credentials is not None
+ assert credentials._jwt_credentials.token == credentials.token