Skip to content

Commit 65e5b34

Browse files
fix: ADC with impersonated workforce pools (#877)
While service account impersonation is uncommonly used with workforce pool external credentials, there is a bug where the following commands raise exceptions when impersonated workforce pools are used: - `google.auth.default()` - `google.auth.load_credentials_from_file()` The issue is due to `google.auth.aws.Credentials` not supporting the `workforce_pool_user_project` argument in the constructor, unlike `google.auth.identity_pool.Credentials`. This was indirectly passed here: https://github.com/googleapis/google-auth-library-python/blob/a37ff00d7afd6c7aac2d0fab29e05708bbc068be/google/auth/external_account.py#L395 Causing a TypeError to be raised (we only catch ValueError). Updated the credential determination logic to explicitly check the subject token type. This is a more reliable indicator instead of a try/catch. Increased unit test coverage in tests/test__default.py to cover these credentials.
1 parent 0b7d6b3 commit 65e5b34

2 files changed

Lines changed: 195 additions & 3 deletions

File tree

packages/google-auth/google/auth/_default.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
added. Or you can use service accounts instead. For more information \
5555
about service accounts, see https://cloud.google.com/docs/authentication/"""
5656

57+
# The subject token type used for AWS external_account credentials.
58+
_AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
59+
5760

5861
def _warn_about_problematic_credentials(credentials):
5962
"""Determines if the credentials are problematic.
@@ -321,14 +324,14 @@ def _get_external_account_credentials(
321324
is in the wrong format or is missing required information.
322325
"""
323326
# There are currently 2 types of external_account credentials.
324-
try:
327+
if info.get("subject_token_type") == _AWS_SUBJECT_TOKEN_TYPE:
325328
# Check if configuration corresponds to an AWS credentials.
326329
from google.auth import aws
327330

328331
credentials = aws.Credentials.from_info(
329332
info, scopes=scopes, default_scopes=default_scopes
330333
)
331-
except ValueError:
334+
else:
332335
try:
333336
# Check if configuration corresponds to an Identity Pool credentials.
334337
from google.auth import identity_pool

packages/google-auth/tests/test__default.py

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
SUBJECT_TOKEN_TEXT_FILE = os.path.join(DATA_DIR, "external_subject_token.txt")
5656
TOKEN_URL = "https://sts.googleapis.com/v1/token"
5757
AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
58+
WORKFORCE_AUDIENCE = (
59+
"//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID"
60+
)
61+
WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER"
5862
REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone"
5963
SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"
6064
CRED_VERIFICATION_URL = (
@@ -79,6 +83,49 @@
7983
"regional_cred_verification_url": CRED_VERIFICATION_URL,
8084
},
8185
}
86+
SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com"
87+
SERVICE_ACCOUNT_IMPERSONATION_URL = (
88+
"https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
89+
+ "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
90+
)
91+
IMPERSONATED_IDENTITY_POOL_DATA = {
92+
"type": "external_account",
93+
"audience": AUDIENCE,
94+
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
95+
"token_url": TOKEN_URL,
96+
"credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
97+
"service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
98+
}
99+
IMPERSONATED_AWS_DATA = {
100+
"type": "external_account",
101+
"audience": AUDIENCE,
102+
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
103+
"token_url": TOKEN_URL,
104+
"credential_source": {
105+
"environment_id": "aws1",
106+
"region_url": REGION_URL,
107+
"url": SECURITY_CREDS_URL,
108+
"regional_cred_verification_url": CRED_VERIFICATION_URL,
109+
},
110+
"service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
111+
}
112+
IDENTITY_POOL_WORKFORCE_DATA = {
113+
"type": "external_account",
114+
"audience": WORKFORCE_AUDIENCE,
115+
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
116+
"token_url": TOKEN_URL,
117+
"credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
118+
"workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
119+
}
120+
IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA = {
121+
"type": "external_account",
122+
"audience": WORKFORCE_AUDIENCE,
123+
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
124+
"token_url": TOKEN_URL,
125+
"credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
126+
"service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
127+
"workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
128+
}
82129

83130
MOCK_CREDENTIALS = mock.Mock(spec=credentials.CredentialsWithQuotaProject)
84131
MOCK_CREDENTIALS.with_quota_project.return_value = MOCK_CREDENTIALS
@@ -256,6 +303,68 @@ def test_load_credentials_from_file_external_account_aws(get_project_id, tmpdir)
256303
assert get_project_id.called
257304

258305

306+
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
307+
def test_load_credentials_from_file_external_account_identity_pool_impersonated(
308+
get_project_id, tmpdir
309+
):
310+
config_file = tmpdir.join("config.json")
311+
config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA))
312+
credentials, project_id = _default.load_credentials_from_file(str(config_file))
313+
314+
assert isinstance(credentials, identity_pool.Credentials)
315+
assert not credentials.is_user
316+
assert not credentials.is_workforce_pool
317+
# Since no scopes are specified, the project ID cannot be determined.
318+
assert project_id is None
319+
assert get_project_id.called
320+
321+
322+
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
323+
def test_load_credentials_from_file_external_account_aws_impersonated(
324+
get_project_id, tmpdir
325+
):
326+
config_file = tmpdir.join("config.json")
327+
config_file.write(json.dumps(IMPERSONATED_AWS_DATA))
328+
credentials, project_id = _default.load_credentials_from_file(str(config_file))
329+
330+
assert isinstance(credentials, aws.Credentials)
331+
assert not credentials.is_user
332+
assert not credentials.is_workforce_pool
333+
# Since no scopes are specified, the project ID cannot be determined.
334+
assert project_id is None
335+
assert get_project_id.called
336+
337+
338+
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
339+
def test_load_credentials_from_file_external_account_workforce(get_project_id, tmpdir):
340+
config_file = tmpdir.join("config.json")
341+
config_file.write(json.dumps(IDENTITY_POOL_WORKFORCE_DATA))
342+
credentials, project_id = _default.load_credentials_from_file(str(config_file))
343+
344+
assert isinstance(credentials, identity_pool.Credentials)
345+
assert credentials.is_user
346+
assert credentials.is_workforce_pool
347+
# Since no scopes are specified, the project ID cannot be determined.
348+
assert project_id is None
349+
assert get_project_id.called
350+
351+
352+
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
353+
def test_load_credentials_from_file_external_account_workforce_impersonated(
354+
get_project_id, tmpdir
355+
):
356+
config_file = tmpdir.join("config.json")
357+
config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA))
358+
credentials, project_id = _default.load_credentials_from_file(str(config_file))
359+
360+
assert isinstance(credentials, identity_pool.Credentials)
361+
assert not credentials.is_user
362+
assert credentials.is_workforce_pool
363+
# Since no scopes are specified, the project ID cannot be determined.
364+
assert project_id is None
365+
assert get_project_id.called
366+
367+
259368
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
260369
def test_load_credentials_from_file_external_account_with_user_and_default_scopes(
261370
get_project_id, tmpdir
@@ -718,18 +827,98 @@ def test_default_no_app_engine_compute_engine_module(unused_get):
718827

719828

720829
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
721-
def test_default_environ_external_credentials(get_project_id, monkeypatch, tmpdir):
830+
def test_default_environ_external_credentials_identity_pool(
831+
get_project_id, monkeypatch, tmpdir
832+
):
722833
config_file = tmpdir.join("config.json")
723834
config_file.write(json.dumps(IDENTITY_POOL_DATA))
724835
monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
725836

726837
credentials, project_id = _default.default()
727838

728839
assert isinstance(credentials, identity_pool.Credentials)
840+
assert not credentials.is_user
841+
assert not credentials.is_workforce_pool
729842
# Without scopes, project ID cannot be determined.
730843
assert project_id is None
731844

732845

846+
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
847+
def test_default_environ_external_credentials_identity_pool_impersonated(
848+
get_project_id, monkeypatch, tmpdir
849+
):
850+
config_file = tmpdir.join("config.json")
851+
config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA))
852+
monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
853+
854+
credentials, project_id = _default.default(
855+
scopes=["https://www.google.com/calendar/feeds"]
856+
)
857+
858+
assert isinstance(credentials, identity_pool.Credentials)
859+
assert not credentials.is_user
860+
assert not credentials.is_workforce_pool
861+
assert project_id is mock.sentinel.project_id
862+
assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
863+
864+
865+
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
866+
def test_default_environ_external_credentials_aws_impersonated(
867+
get_project_id, monkeypatch, tmpdir
868+
):
869+
config_file = tmpdir.join("config.json")
870+
config_file.write(json.dumps(IMPERSONATED_AWS_DATA))
871+
monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
872+
873+
credentials, project_id = _default.default(
874+
scopes=["https://www.google.com/calendar/feeds"]
875+
)
876+
877+
assert isinstance(credentials, aws.Credentials)
878+
assert not credentials.is_user
879+
assert not credentials.is_workforce_pool
880+
assert project_id is mock.sentinel.project_id
881+
assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
882+
883+
884+
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
885+
def test_default_environ_external_credentials_workforce(
886+
get_project_id, monkeypatch, tmpdir
887+
):
888+
config_file = tmpdir.join("config.json")
889+
config_file.write(json.dumps(IDENTITY_POOL_WORKFORCE_DATA))
890+
monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
891+
892+
credentials, project_id = _default.default(
893+
scopes=["https://www.google.com/calendar/feeds"]
894+
)
895+
896+
assert isinstance(credentials, identity_pool.Credentials)
897+
assert credentials.is_user
898+
assert credentials.is_workforce_pool
899+
assert project_id is mock.sentinel.project_id
900+
assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
901+
902+
903+
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
904+
def test_default_environ_external_credentials_workforce_impersonated(
905+
get_project_id, monkeypatch, tmpdir
906+
):
907+
config_file = tmpdir.join("config.json")
908+
config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA))
909+
monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
910+
911+
credentials, project_id = _default.default(
912+
scopes=["https://www.google.com/calendar/feeds"]
913+
)
914+
915+
assert isinstance(credentials, identity_pool.Credentials)
916+
assert not credentials.is_user
917+
assert credentials.is_workforce_pool
918+
assert project_id is mock.sentinel.project_id
919+
assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
920+
921+
733922
@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
734923
def test_default_environ_external_credentials_with_user_and_default_scopes_and_quota_project_id(
735924
get_project_id, monkeypatch, tmpdir

0 commit comments

Comments
 (0)