diff options
Diffstat (limited to 'system_tests')
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 Binary files differnew file mode 100644 index 0000000..5f20b1e --- /dev/null +++ b/system_tests/secrets.tar.enc 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 Binary files differnew file mode 100644 index 0000000..29e0692 --- /dev/null +++ b/system_tests/system_tests_sync/secrets.tar.enc 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 |