fix: only add IAM scope to credentials that can change scopes (#451)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index f95b1f1..bd92ca8 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -43,21 +43,27 @@
$ nox -f system_tests/noxfile.py -s service_account
+
+Project and Credentials Setup
+-------------------------------
+
+Enable the IAM Service Account Credentials API on the project.
+
To run system tests locally, you will need to set up a data directory ::
$ mkdir system_tests/data
-Add a service account file and authorized user file to the data directory.
-Your directory should look like this ::
+Your directory should look like this. Follow the instructions below for creating each file. ::
system_tests/
data/
- service_account.json
authorized_user.json
+ impersonated_service_account.json
+ service_account.json
-The files must be named exactly ``service_account.json``
-and ``authorized_user.json``. See `Creating and Managing Service Account Keys`_ for how to
-obtain a service account.
+
+``authorized_user.json``
+~~~~~~~~~~~~~~~~~~~~~~~~
Use the `gcloud CLI`_ to get an authorized user file ::
@@ -65,15 +71,41 @@
You will see something like::
- Credentials saved to file: [/usr/local/home/.config/gcloud/application_default_credentials.json]```
+ Credentials saved to file: [/usr/local/home/.config/gcloud/application_default_credentials.json]
Copy the contents of the file to ``authorized_user.json``.
-.. _Creating and Managing Service Account Keys: https://cloud.google.com/iam/docs/creating-managing-service-account-keys
+Open the IAM page of the Google Cloud Console. Grant the user the `Service Account Token Creator Role`.
+This will allow the user to impersonate service accounts on the project.
+
.. _gcloud CLI: https://cloud.google.com/sdk/gcloud/
+
+``service_account.json``
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Follow `Creating and Managing Service Account Keys`_ to create a service account.
+
+Copy the credentials file to ``service_account.json``.
+
+Grant the account associated with ``service_account.json`` the following roles.
+
+- App Engine Admin (for App Engine tests)
+- Service Account Token Creator (for impersonated credentials tests)
+- Pub/Sub Viewer (for gRPC tests)
+- Storage Object Viewer (for impersonated credentials tests)
+
+``impersonated_service_account.json``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Follow `Creating and Managing Service Account Keys`_ to create a service account.
+
+Copy the credentials file to ``impersonated_service_account.json``.
+
+.. _Creating and Managing Service Account Keys: https://cloud.google.com/iam/docs/creating-managing-service-account-keys
+
App Engine System Tests
-^^^^^^^^^^^^^^^^^^^^^^^
+~~~~~~~~~~~~~~~~~~~~~~~~
To run the App Engine tests, you wil need to deploy a default App Engine service.
If you already have a default service associated with your project, you can skip this step.
diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py
index bc7031e..1bb6b82 100644
--- a/google/auth/impersonated_credentials.py
+++ b/google/auth/impersonated_credentials.py
@@ -205,7 +205,11 @@
super(Credentials, self).__init__()
self._source_credentials = copy.copy(source_credentials)
- self._source_credentials._scopes = _IAM_SCOPE
+ # Service account source credentials must have the _IAM_SCOPE
+ # added to refresh correctly. User credentials cannot have
+ # their original scopes modified.
+ if isinstance(self._source_credentials, credentials.Scoped):
+ self._source_credentials = self._source_credentials.with_scopes(_IAM_SCOPE)
self._target_principal = target_principal
self._target_scopes = target_scopes
self._delegates = delegates
diff --git a/system_tests/conftest.py b/system_tests/conftest.py
index 1893007..02de846 100644
--- a/system_tests/conftest.py
+++ b/system_tests/conftest.py
@@ -25,6 +25,9 @@
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")
AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
URLLIB3_HTTP = urllib3.PoolManager(retries=False)
@@ -40,6 +43,12 @@
@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
diff --git a/system_tests/noxfile.py b/system_tests/noxfile.py
index e37049e..8110632 100644
--- a/system_tests/noxfile.py
+++ b/system_tests/noxfile.py
@@ -170,7 +170,8 @@
# Test sesssions
TEST_DEPENDENCIES = ["pytest", "requests"]
-PYTHON_VERSIONS=['2.7', '3.7']
+PYTHON_VERSIONS = ["2.7", "3.7"]
+
@nox.session(python=PYTHON_VERSIONS)
def service_account(session):
@@ -187,6 +188,13 @@
@nox.session(python=PYTHON_VERSIONS)
+def impersonated_credentials(session):
+ session.install(*TEST_DEPENDENCIES)
+ session.install(LIBRARY_DIR)
+ session.run("pytest", "test_impersonated_credentials.py")
+
+
+@nox.session(python=PYTHON_VERSIONS)
def default_explicit_service_account(session):
session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
session.env[EXPECT_PROJECT_ENV] = "1"
diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc
index 1106f8a..af10c71 100644
--- a/system_tests/secrets.tar.enc
+++ b/system_tests/secrets.tar.enc
Binary files differ
diff --git a/system_tests/test_impersonated_credentials.py b/system_tests/test_impersonated_credentials.py
new file mode 100644
index 0000000..6689e89
--- /dev/null
+++ b/system_tests/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/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py
index 1cfcc7c..31075ca 100644
--- a/tests/test_impersonated_credentials.py
+++ b/tests/test_impersonated_credentials.py
@@ -26,6 +26,7 @@
from google.auth import impersonated_credentials
from google.auth import transport
from google.auth.impersonated_credentials import Credentials
+from google.oauth2 import credentials
from google.oauth2 import service_account
DATA_DIR = os.path.join(os.path.dirname(__file__), "", "data")
@@ -102,17 +103,30 @@
SOURCE_CREDENTIALS = service_account.Credentials(
SIGNER, SERVICE_ACCOUNT_EMAIL, TOKEN_URI
)
+ USER_SOURCE_CREDENTIALS = credentials.Credentials(token="ABCDE")
- def make_credentials(self, lifetime=LIFETIME, target_principal=TARGET_PRINCIPAL):
+ def make_credentials(
+ self,
+ source_credentials=SOURCE_CREDENTIALS,
+ lifetime=LIFETIME,
+ target_principal=TARGET_PRINCIPAL,
+ ):
return Credentials(
- source_credentials=self.SOURCE_CREDENTIALS,
+ source_credentials=source_credentials,
target_principal=target_principal,
target_scopes=self.TARGET_SCOPES,
delegates=self.DELEGATES,
lifetime=lifetime,
)
+ def test_make_from_user_credentials(self):
+ credentials = self.make_credentials(
+ source_credentials=self.USER_SOURCE_CREDENTIALS
+ )
+ assert not credentials.valid
+ assert credentials.expired
+
def test_default_state(self):
credentials = self.make_credentials()
assert not credentials.valid