Skip to content

Commit bd342ea

Browse files
committed
Additional unit tests and update some old ones.
1 parent c281fee commit bd342ea

File tree

8 files changed

+237
-69
lines changed

8 files changed

+237
-69
lines changed

google/oauth2/service_account.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -482,10 +482,14 @@ def refresh(self, request):
482482
google.auth.exceptions.RefreshError: If the credentials could
483483
not be refreshed.
484484
"""
485-
# Determine if we're going to use a self-signed JWT.
486-
use_ssjwt = self._use_self_signed_jwt()
487-
488-
if use_ssjwt and self._is_trust_boundary_lookup_required():
485+
# This is a special path for self-signed JWTs that need to look up a trust boundary.
486+
# The `_subject` check is to ensure we are not in a domain-wide
487+
# delegation flow, which uses a different authentication mechanism.
488+
if (
489+
self._always_use_jwt_access
490+
and self._subject is None
491+
and self._is_trust_boundary_lookup_required()
492+
):
489493
# Special case: self-signed JWT with trust boundary.
490494
# 1. Create a temporary self-signed JWT for the IAM API.
491495
iam_audience = "https://iamcredentials.{}/".format(self._universe_domain)
@@ -496,9 +500,12 @@ def refresh(self, request):
496500
# We temporarily set self.token for the base lookup method.
497501
# The base lookup method will call self.apply() which adds the
498502
# authorization header.
499-
with _helpers.update_property(self, "token", iam_jwt_creds.token.decode()):
500-
# This will call _lookup_trust_boundary and set self._trust_boundary
503+
original_token = self.token
504+
self.token = iam_jwt_creds.token.decode()
505+
try:
501506
self._refresh_trust_boundary(request)
507+
finally:
508+
self.token = original_token
502509

503510
# 3. Now, refresh the original self-signed JWT for the target API.
504511
self._refresh_token(request)

tests/compute_engine/test_credentials.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,18 @@ def test_with_universe_domain(self):
216216
assert creds.universe_domain == "universe_domain"
217217
assert creds._universe_domain_cached
218218

219+
def test_with_trust_boundary(self):
220+
creds = self.credentials_with_all_fields
221+
new_boundary = {"encodedLocations": "new_boundary"}
222+
new_creds = creds.with_trust_boundary(new_boundary)
223+
224+
assert new_creds is not creds
225+
assert new_creds._trust_boundary == new_boundary
226+
assert new_creds._service_account_email == creds._service_account_email
227+
assert new_creds._quota_project_id == creds._quota_project_id
228+
assert new_creds._scopes == creds._scopes
229+
assert new_creds._default_scopes == creds._default_scopes
230+
219231
def test_token_usage_metrics(self):
220232
self.credentials.token = "token"
221233
self.credentials.expiry = None

tests/oauth2/test_service_account.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,18 @@ def test__with_always_use_jwt_access_non_default_universe_domain(self):
270270
"always_use_jwt_access should be True for non-default universe domain"
271271
)
272272

273+
def test_with_trust_boundary(self):
274+
credentials = self.make_credentials()
275+
new_boundary = {"encodedLocations": "new_boundary"}
276+
new_credentials = credentials.with_trust_boundary(new_boundary)
277+
278+
assert new_credentials is not credentials
279+
assert new_credentials._trust_boundary == new_boundary
280+
assert new_credentials._signer == credentials._signer
281+
assert (
282+
new_credentials.service_account_email == credentials.service_account_email
283+
)
284+
273285
def test__make_authorization_grant_assertion(self):
274286
credentials = self.make_credentials()
275287
token = credentials._make_authorization_grant_assertion()
@@ -851,6 +863,74 @@ def test_refresh_trust_boundary_lookup_fails_with_cached_data(
851863
},
852864
) # Lookup should have been attempted again
853865

866+
@mock.patch("google.oauth2._client._lookup_trust_boundary")
867+
def test_refresh_with_self_signed_jwt_and_trust_boundary(
868+
self, mock_lookup_trust_boundary
869+
):
870+
# --- Setup ---
871+
# Create credentials that use self-signed JWT and have no initial boundary.
872+
credentials = self.make_credentials(trust_boundary=None)
873+
credentials._always_use_jwt_access = True
874+
credentials._scopes = ["https://www.googleapis.com/auth/devstorage.read_only"]
875+
request = mock.create_autospec(transport.Request, instance=True)
876+
877+
# Mock the trust boundary lookup to return a valid value.
878+
mock_lookup_trust_boundary.return_value = self.VALID_TRUST_BOUNDARY
879+
880+
# Mock the two JWTs that will be created: one for the IAM lookup
881+
# and one for the final token.
882+
iam_jwt_creds_mock = mock.MagicMock(spec=jwt.Credentials, token=b"iam_token")
883+
final_jwt_creds_mock = mock.MagicMock(
884+
spec=jwt.Credentials,
885+
token=b"final_token",
886+
expiry=_helpers.utcnow() + datetime.timedelta(hours=1),
887+
)
888+
from_signing_credentials_mock = mock.patch(
889+
"google.auth.jwt.Credentials.from_signing_credentials",
890+
side_effect=[iam_jwt_creds_mock, final_jwt_creds_mock],
891+
)
892+
env_mock = mock.patch.dict(
893+
os.environ, {environment_vars.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED: "true"}
894+
)
895+
896+
# Refresh credential to trigger creating the self-signed jwt token and
897+
# fetching the trust boundary data.
898+
with from_signing_credentials_mock as mock_from_signing, env_mock:
899+
credentials.refresh(request)
900+
901+
# --- Assert ---
902+
# 1. Verify the temporary IAM JWT was created and used for the lookup.
903+
iam_call = mock_from_signing.call_args_list[0]
904+
assert iam_call.args[0] is credentials
905+
assert iam_call.args[1] == "https://iamcredentials.googleapis.com/"
906+
iam_jwt_creds_mock.refresh.assert_called_once_with(request)
907+
908+
mock_lookup_trust_boundary.assert_called_once()
909+
_, lookup_kwargs = mock_lookup_trust_boundary.call_args
910+
assert lookup_kwargs["headers"]["authorization"] == "Bearer iam_token"
911+
912+
# 2. Verify the final, audience-specific JWT was created and refreshed.
913+
final_token_call = mock_from_signing.call_args_list[1]
914+
assert final_token_call.args[0] is credentials
915+
assert final_token_call.args[1] is None # Audience is derived from scopes
916+
assert final_token_call.kwargs["additional_claims"] == {
917+
"scope": " ".join(credentials._scopes)
918+
}
919+
final_jwt_creds_mock.refresh.assert_called_once_with(request)
920+
921+
# 3. Verify the final state of the credentials object.
922+
assert credentials.token == "final_token"
923+
assert credentials._trust_boundary == self.VALID_TRUST_BOUNDARY
924+
925+
def test_build_trust_boundary_lookup_url_no_email(self):
926+
credentials = self.make_credentials()
927+
credentials._service_account_email = None
928+
929+
with pytest.raises(ValueError) as excinfo:
930+
credentials._build_trust_boundary_lookup_url()
931+
932+
assert "Service account email is required" in str(excinfo.value)
933+
854934

855935
class TestIDTokenCredentials(object):
856936
SERVICE_ACCOUNT_EMAIL = "service-account@example.com"
@@ -981,6 +1061,19 @@ def test_with_token_uri(self):
9811061
creds_with_new_token_uri = credentials.with_token_uri(new_token_uri)
9821062
assert creds_with_new_token_uri._token_uri == new_token_uri
9831063

1064+
def test_with_trust_boundary(self):
1065+
credentials = self.make_credentials()
1066+
new_boundary = {"encodedLocations": "new_boundary"}
1067+
new_credentials = credentials.with_trust_boundary(new_boundary)
1068+
1069+
assert new_credentials is not credentials
1070+
assert new_credentials._trust_boundary == new_boundary
1071+
assert new_credentials._signer == credentials._signer
1072+
assert (
1073+
new_credentials.service_account_email == credentials.service_account_email
1074+
)
1075+
assert new_credentials._target_audience == credentials._target_audience
1076+
9841077
def test__make_authorization_grant_assertion(self):
9851078
credentials = self.make_credentials()
9861079
token = credentials._make_authorization_grant_assertion()

tests/test_aws.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
SERVICE_ACCOUNT_IMPERSONATION_URL_BASE = (
4343
"https://us-east1-iamcredentials.googleapis.com"
4444
)
45-
SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE = "/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
46-
SERVICE_ACCOUNT_EMAIL
45+
SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE = (
46+
"/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
47+
SERVICE_ACCOUNT_EMAIL
48+
)
4749
)
4850
SERVICE_ACCOUNT_IMPERSONATION_URL = (
4951
SERVICE_ACCOUNT_IMPERSONATION_URL_BASE + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
@@ -920,7 +922,7 @@ def assert_token_request_kwargs(
920922
assert request_kwargs["body"] is not None
921923
body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
922924
assert len(body_tuples) == len(request_data.keys())
923-
for (k, v) in body_tuples:
925+
for k, v in body_tuples:
924926
assert v.decode("utf-8") == request_data[k.decode("utf-8")]
925927

926928
@classmethod
@@ -1344,9 +1346,9 @@ def test_retrieve_subject_token_success_temp_creds_no_environment_vars_idmsv2(
13441346
imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
13451347
)
13461348
credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
1347-
credential_source_token_url[
1348-
"imdsv2_session_token_url"
1349-
] = IMDSV2_SESSION_TOKEN_URL
1349+
credential_source_token_url["imdsv2_session_token_url"] = (
1350+
IMDSV2_SESSION_TOKEN_URL
1351+
)
13501352
credentials = self.make_credentials(
13511353
credential_source=credential_source_token_url
13521354
)
@@ -1452,9 +1454,9 @@ def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_secr
14521454
imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
14531455
)
14541456
credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
1455-
credential_source_token_url[
1456-
"imdsv2_session_token_url"
1457-
] = IMDSV2_SESSION_TOKEN_URL
1457+
credential_source_token_url["imdsv2_session_token_url"] = (
1458+
IMDSV2_SESSION_TOKEN_URL
1459+
)
14581460
credentials = self.make_credentials(
14591461
credential_source=credential_source_token_url
14601462
)
@@ -1509,9 +1511,9 @@ def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_acce
15091511
imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
15101512
)
15111513
credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
1512-
credential_source_token_url[
1513-
"imdsv2_session_token_url"
1514-
] = IMDSV2_SESSION_TOKEN_URL
1514+
credential_source_token_url["imdsv2_session_token_url"] = (
1515+
IMDSV2_SESSION_TOKEN_URL
1516+
)
15151517
credentials = self.make_credentials(
15161518
credential_source=credential_source_token_url
15171519
)
@@ -1560,9 +1562,9 @@ def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_cred
15601562
imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
15611563
)
15621564
credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
1563-
credential_source_token_url[
1564-
"imdsv2_session_token_url"
1565-
] = IMDSV2_SESSION_TOKEN_URL
1565+
credential_source_token_url["imdsv2_session_token_url"] = (
1566+
IMDSV2_SESSION_TOKEN_URL
1567+
)
15661568
credentials = self.make_credentials(
15671569
credential_source=credential_source_token_url
15681570
)
@@ -1611,9 +1613,9 @@ def test_retrieve_subject_token_success_temp_creds_idmsv2(self, utcnow):
16111613
role_status=http_client.OK, role_name=self.AWS_ROLE
16121614
)
16131615
credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
1614-
credential_source_token_url[
1615-
"imdsv2_session_token_url"
1616-
] = IMDSV2_SESSION_TOKEN_URL
1616+
credential_source_token_url["imdsv2_session_token_url"] = (
1617+
IMDSV2_SESSION_TOKEN_URL
1618+
)
16171619
credentials = self.make_credentials(
16181620
credential_source=credential_source_token_url
16191621
)
@@ -1692,9 +1694,9 @@ def test_retrieve_subject_token_session_error_idmsv2(self, utcnow):
16921694
imdsv2_session_token_data="unauthorized",
16931695
)
16941696
credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
1695-
credential_source_token_url[
1696-
"imdsv2_session_token_url"
1697-
] = IMDSV2_SESSION_TOKEN_URL
1697+
credential_source_token_url["imdsv2_session_token_url"] = (
1698+
IMDSV2_SESSION_TOKEN_URL
1699+
)
16981700
credentials = self.make_credentials(
16991701
credential_source=credential_source_token_url
17001702
)
@@ -2057,7 +2059,9 @@ def test_refresh_success_with_impersonation_ignore_default_scopes(
20572059
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
20582060
"x-goog-user-project": QUOTA_PROJECT_ID,
20592061
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
2060-
"x-allowed-locations": "0x0",
2062+
# TODO(negarb): Uncomment and update when trust boundary is supported
2063+
# for external account credentials.
2064+
# "x-allowed-locations": "0x0",
20612065
}
20622066
impersonation_request_data = {
20632067
"delegates": None,
@@ -2150,7 +2154,7 @@ def test_refresh_success_with_impersonation_use_default_scopes(
21502154
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
21512155
"x-goog-user-project": QUOTA_PROJECT_ID,
21522156
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
2153-
"x-allowed-locations": "0x0",
2157+
# "x-allowed-locations": "0x0",
21542158
}
21552159
impersonation_request_data = {
21562160
"delegates": None,
@@ -2345,7 +2349,7 @@ def test_refresh_success_with_supplier_with_impersonation(
23452349
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
23462350
"x-goog-user-project": QUOTA_PROJECT_ID,
23472351
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
2348-
"x-allowed-locations": "0x0",
2352+
# "x-allowed-locations": "0x0",
23492353
}
23502354
impersonation_request_data = {
23512355
"delegates": None,

tests/test_credentials.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727

2828
class CredentialsImpl(credentials.CredentialsWithTrustBoundary):
29-
def refresh(self, request):
29+
def _refresh_token(self, request):
3030
self.token = request
3131
self.expiry = (
3232
datetime.datetime.utcnow()
@@ -354,7 +354,7 @@ def test_token_state_no_expiry():
354354

355355

356356
class TestCredentialsWithTrustBoundary(object):
357-
@mock.patch.object(_client, "lookup_trust_boundary")
357+
@mock.patch.object(_client, "_lookup_trust_boundary")
358358
def test_lookup_trust_boundary_env_var_not_true(self, mock_lookup_tb):
359359
creds = CredentialsImpl()
360360
request = mock.Mock()
@@ -363,12 +363,12 @@ def test_lookup_trust_boundary_env_var_not_true(self, mock_lookup_tb):
363363
with mock.patch.dict(
364364
os.environ, {environment_vars.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED: "false"}
365365
):
366-
result = creds._lookup_trust_boundary(request)
366+
result = creds._refresh_trust_boundary(request)
367367

368368
assert result is None
369369
mock_lookup_tb.assert_not_called()
370370

371-
@mock.patch.object(_client, "lookup_trust_boundary")
371+
@mock.patch.object(_client, "_lookup_trust_boundary")
372372
def test_lookup_trust_boundary_env_var_missing(self, mock_lookup_tb):
373373
creds = CredentialsImpl()
374374
request = mock.Mock()
@@ -378,12 +378,12 @@ def test_lookup_trust_boundary_env_var_missing(self, mock_lookup_tb):
378378
# Remove the var if it was set by other tests
379379
if environment_vars.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED in os.environ:
380380
del os.environ[environment_vars.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED]
381-
result = creds._lookup_trust_boundary(request)
381+
result = creds._refresh_trust_boundary(request)
382382

383383
assert result is None
384384
mock_lookup_tb.assert_not_called()
385385

386-
@mock.patch.object(_client, "lookup_trust_boundary")
386+
@mock.patch.object(_client, "_lookup_trust_boundary")
387387
def test_lookup_trust_boundary_non_default_universe(self, mock_lookup_tb):
388388
creds = CredentialsImpl()
389389
creds._universe_domain = "my.universe.com" # Non-GDU
@@ -392,12 +392,12 @@ def test_lookup_trust_boundary_non_default_universe(self, mock_lookup_tb):
392392
with mock.patch.dict(
393393
os.environ, {environment_vars.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED: "true"}
394394
):
395-
result = creds._lookup_trust_boundary(request)
395+
result = creds._refresh_trust_boundary(request)
396396

397397
assert result is None
398398
mock_lookup_tb.assert_not_called()
399399

400-
@mock.patch.object(_client, "lookup_trust_boundary")
400+
@mock.patch.object(_client, "_lookup_trust_boundary")
401401
def test_lookup_trust_boundary_calls_client_and_build_url(self, mock_lookup_tb):
402402
creds = CredentialsImpl()
403403
creds.token = "test_token" # For _build_trust_boundary_lookup_url
@@ -422,7 +422,7 @@ def test_lookup_trust_boundary_calls_client_and_build_url(self, mock_lookup_tb):
422422
request, expected_url, headers=expected_headers
423423
)
424424

425-
@mock.patch.object(_client, "lookup_trust_boundary")
425+
@mock.patch.object(_client, "_lookup_trust_boundary")
426426
def test_lookup_trust_boundary_build_url_returns_none(self, mock_lookup_tb):
427427
creds = CredentialsImpl()
428428
request = mock.Mock()

0 commit comments

Comments
 (0)