Skip to content

Commit 82e224b

Browse files
authored
fix: only add IAM scope to credentials that can change scopes (#451)
1 parent b2dd77f commit 82e224b

File tree

7 files changed

+179
-13
lines changed

7 files changed

+179
-13
lines changed

CONTRIBUTING.rst

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,37 +43,69 @@ To run a single session, specify it with ``nox -s``::
4343

4444
$ nox -f system_tests/noxfile.py -s service_account
4545

46+
47+
Project and Credentials Setup
48+
-------------------------------
49+
50+
Enable the IAM Service Account Credentials API on the project.
51+
4652
To run system tests locally, you will need to set up a data directory ::
4753

4854
$ mkdir system_tests/data
4955

50-
Add a service account file and authorized user file to the data directory.
51-
Your directory should look like this ::
56+
Your directory should look like this. Follow the instructions below for creating each file. ::
5257

5358
system_tests/
5459
data/
55-
service_account.json
5660
authorized_user.json
61+
impersonated_service_account.json
62+
service_account.json
5763

58-
The files must be named exactly ``service_account.json``
59-
and ``authorized_user.json``. See `Creating and Managing Service Account Keys`_ for how to
60-
obtain a service account.
64+
65+
``authorized_user.json``
66+
~~~~~~~~~~~~~~~~~~~~~~~~
6167

6268
Use the `gcloud CLI`_ to get an authorized user file ::
6369

6470
$ gcloud auth application-default login --scopes=https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/cloud-platform,openid
6571

6672
You will see something like::
6773

68-
Credentials saved to file: [/usr/local/home/.config/gcloud/application_default_credentials.json]```
74+
Credentials saved to file: [/usr/local/home/.config/gcloud/application_default_credentials.json]
6975

7076
Copy the contents of the file to ``authorized_user.json``.
7177

72-
.. _Creating and Managing Service Account Keys: https://cloud.google.com/iam/docs/creating-managing-service-account-keys
78+
Open the IAM page of the Google Cloud Console. Grant the user the `Service Account Token Creator Role`.
79+
This will allow the user to impersonate service accounts on the project.
80+
7381
.. _gcloud CLI: https://cloud.google.com/sdk/gcloud/
7482

83+
84+
``service_account.json``
85+
~~~~~~~~~~~~~~~~~~~~~~~~
86+
87+
Follow `Creating and Managing Service Account Keys`_ to create a service account.
88+
89+
Copy the credentials file to ``service_account.json``.
90+
91+
Grant the account associated with ``service_account.json`` the following roles.
92+
93+
- App Engine Admin (for App Engine tests)
94+
- Service Account Token Creator (for impersonated credentials tests)
95+
- Pub/Sub Viewer (for gRPC tests)
96+
- Storage Object Viewer (for impersonated credentials tests)
97+
98+
``impersonated_service_account.json``
99+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
100+
101+
Follow `Creating and Managing Service Account Keys`_ to create a service account.
102+
103+
Copy the credentials file to ``impersonated_service_account.json``.
104+
105+
.. _Creating and Managing Service Account Keys: https://cloud.google.com/iam/docs/creating-managing-service-account-keys
106+
75107
App Engine System Tests
76-
^^^^^^^^^^^^^^^^^^^^^^^
108+
~~~~~~~~~~~~~~~~~~~~~~~~
77109

78110
To run the App Engine tests, you wil need to deploy a default App Engine service.
79111
If you already have a default service associated with your project, you can skip this step.

google/auth/impersonated_credentials.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,11 @@ def __init__(
205205
super(Credentials, self).__init__()
206206

207207
self._source_credentials = copy.copy(source_credentials)
208-
self._source_credentials._scopes = _IAM_SCOPE
208+
# Service account source credentials must have the _IAM_SCOPE
209+
# added to refresh correctly. User credentials cannot have
210+
# their original scopes modified.
211+
if isinstance(self._source_credentials, credentials.Scoped):
212+
self._source_credentials = self._source_credentials.with_scopes(_IAM_SCOPE)
209213
self._target_principal = target_principal
210214
self._target_scopes = target_scopes
211215
self._delegates = delegates

system_tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525

2626
HERE = os.path.dirname(__file__)
2727
DATA_DIR = os.path.join(HERE, "data")
28+
IMPERSONATED_SERVICE_ACCOUNT_FILE = os.path.join(
29+
DATA_DIR, "impersonated_service_account.json"
30+
)
2831
SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
2932
AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
3033
URLLIB3_HTTP = urllib3.PoolManager(retries=False)
@@ -39,6 +42,12 @@ def service_account_file():
3942
yield SERVICE_ACCOUNT_FILE
4043

4144

45+
@pytest.fixture
46+
def impersonated_service_account_file():
47+
"""The full path to a valid service account key file."""
48+
yield IMPERSONATED_SERVICE_ACCOUNT_FILE
49+
50+
4251
@pytest.fixture
4352
def authorized_user_file():
4453
"""The full path to a valid authorized user file."""

system_tests/noxfile.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ def configure_cloud_sdk(session, application_default_credentials, project=False)
170170
# Test sesssions
171171

172172
TEST_DEPENDENCIES = ["pytest", "requests"]
173-
PYTHON_VERSIONS=['2.7', '3.7']
173+
PYTHON_VERSIONS = ["2.7", "3.7"]
174+
174175

175176
@nox.session(python=PYTHON_VERSIONS)
176177
def service_account(session):
@@ -186,6 +187,13 @@ def oauth2_credentials(session):
186187
session.run("pytest", "test_oauth2_credentials.py")
187188

188189

190+
@nox.session(python=PYTHON_VERSIONS)
191+
def impersonated_credentials(session):
192+
session.install(*TEST_DEPENDENCIES)
193+
session.install(LIBRARY_DIR)
194+
session.run("pytest", "test_impersonated_credentials.py")
195+
196+
189197
@nox.session(python=PYTHON_VERSIONS)
190198
def default_explicit_service_account(session):
191199
session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE

system_tests/secrets.tar.enc

0 Bytes
Binary file not shown.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright 2020 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import pytest
17+
18+
import google.oauth2.credentials
19+
from google.oauth2 import service_account
20+
import google.auth.impersonated_credentials
21+
from google.auth import _helpers
22+
23+
24+
GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
25+
26+
27+
@pytest.fixture
28+
def service_account_credentials(service_account_file):
29+
yield service_account.Credentials.from_service_account_file(service_account_file)
30+
31+
32+
@pytest.fixture
33+
def impersonated_service_account_credentials(impersonated_service_account_file):
34+
yield service_account.Credentials.from_service_account_file(
35+
impersonated_service_account_file
36+
)
37+
38+
39+
def test_refresh_with_user_credentials_as_source(
40+
authorized_user_file,
41+
impersonated_service_account_credentials,
42+
http_request,
43+
token_info,
44+
):
45+
with open(authorized_user_file, "r") as fh:
46+
info = json.load(fh)
47+
48+
source_credentials = google.oauth2.credentials.Credentials(
49+
None,
50+
refresh_token=info["refresh_token"],
51+
token_uri=GOOGLE_OAUTH2_TOKEN_ENDPOINT,
52+
client_id=info["client_id"],
53+
client_secret=info["client_secret"],
54+
# The source credential needs this scope for the generateAccessToken request
55+
# The user must also have `Service Account Token Creator` on the project
56+
# that owns the impersonated service account.
57+
# See https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
58+
scopes=["https://www.googleapis.com/auth/cloud-platform"],
59+
)
60+
61+
source_credentials.refresh(http_request)
62+
63+
target_scopes = [
64+
"https://www.googleapis.com/auth/devstorage.read_only",
65+
"https://www.googleapis.com/auth/analytics",
66+
]
67+
target_credentials = google.auth.impersonated_credentials.Credentials(
68+
source_credentials=source_credentials,
69+
target_principal=impersonated_service_account_credentials.service_account_email,
70+
target_scopes=target_scopes,
71+
lifetime=100,
72+
)
73+
74+
target_credentials.refresh(http_request)
75+
assert target_credentials.token
76+
77+
78+
def test_refresh_with_service_account_credentials_as_source(
79+
http_request,
80+
service_account_credentials,
81+
impersonated_service_account_credentials,
82+
token_info,
83+
):
84+
source_credentials = service_account_credentials.with_scopes(["email"])
85+
source_credentials.refresh(http_request)
86+
assert source_credentials.token
87+
88+
target_scopes = [
89+
"https://www.googleapis.com/auth/devstorage.read_only",
90+
"https://www.googleapis.com/auth/analytics",
91+
]
92+
target_credentials = google.auth.impersonated_credentials.Credentials(
93+
source_credentials=source_credentials,
94+
target_principal=impersonated_service_account_credentials.service_account_email,
95+
target_scopes=target_scopes,
96+
)
97+
98+
target_credentials.refresh(http_request)
99+
assert target_credentials.token

tests/test_impersonated_credentials.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from google.auth import impersonated_credentials
2727
from google.auth import transport
2828
from google.auth.impersonated_credentials import Credentials
29+
from google.oauth2 import credentials
2930
from google.oauth2 import service_account
3031

3132
DATA_DIR = os.path.join(os.path.dirname(__file__), "", "data")
@@ -102,17 +103,30 @@ class TestImpersonatedCredentials(object):
102103
SOURCE_CREDENTIALS = service_account.Credentials(
103104
SIGNER, SERVICE_ACCOUNT_EMAIL, TOKEN_URI
104105
)
106+
USER_SOURCE_CREDENTIALS = credentials.Credentials(token="ABCDE")
105107

106-
def make_credentials(self, lifetime=LIFETIME, target_principal=TARGET_PRINCIPAL):
108+
def make_credentials(
109+
self,
110+
source_credentials=SOURCE_CREDENTIALS,
111+
lifetime=LIFETIME,
112+
target_principal=TARGET_PRINCIPAL,
113+
):
107114

108115
return Credentials(
109-
source_credentials=self.SOURCE_CREDENTIALS,
116+
source_credentials=source_credentials,
110117
target_principal=target_principal,
111118
target_scopes=self.TARGET_SCOPES,
112119
delegates=self.DELEGATES,
113120
lifetime=lifetime,
114121
)
115122

123+
def test_make_from_user_credentials(self):
124+
credentials = self.make_credentials(
125+
source_credentials=self.USER_SOURCE_CREDENTIALS
126+
)
127+
assert not credentials.valid
128+
assert credentials.expired
129+
116130
def test_default_state(self):
117131
credentials = self.make_credentials()
118132
assert not credentials.valid

0 commit comments

Comments
 (0)