1# Copyright 2016 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
15import json
16import os
17
18import mock
19import pytest
20
21from google.auth import _default
22from google.auth import app_engine
23from google.auth import aws
24from google.auth import compute_engine
25from google.auth import credentials
26from google.auth import environment_vars
27from google.auth import exceptions
28from google.auth import external_account
29from google.auth import identity_pool
30from google.oauth2 import service_account
31import google.oauth2.credentials
32
33
34DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
35AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
36
37with open(AUTHORIZED_USER_FILE) as fh:
38    AUTHORIZED_USER_FILE_DATA = json.load(fh)
39
40AUTHORIZED_USER_CLOUD_SDK_FILE = os.path.join(
41    DATA_DIR, "authorized_user_cloud_sdk.json"
42)
43
44AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE = os.path.join(
45    DATA_DIR, "authorized_user_cloud_sdk_with_quota_project_id.json"
46)
47
48SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
49
50CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json")
51
52with open(SERVICE_ACCOUNT_FILE) as fh:
53    SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
54
55SUBJECT_TOKEN_TEXT_FILE = os.path.join(DATA_DIR, "external_subject_token.txt")
56TOKEN_URL = "https://sts.googleapis.com/v1/token"
57AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
58WORKFORCE_AUDIENCE = (
59    "//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID"
60)
61WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER"
62REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone"
63SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"
64CRED_VERIFICATION_URL = (
65    "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
66)
67IDENTITY_POOL_DATA = {
68    "type": "external_account",
69    "audience": AUDIENCE,
70    "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
71    "token_url": TOKEN_URL,
72    "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
73}
74AWS_DATA = {
75    "type": "external_account",
76    "audience": AUDIENCE,
77    "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
78    "token_url": TOKEN_URL,
79    "credential_source": {
80        "environment_id": "aws1",
81        "region_url": REGION_URL,
82        "url": SECURITY_CREDS_URL,
83        "regional_cred_verification_url": CRED_VERIFICATION_URL,
84    },
85}
86SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com"
87SERVICE_ACCOUNT_IMPERSONATION_URL = (
88    "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
89    + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
90)
91IMPERSONATED_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}
99IMPERSONATED_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}
112IDENTITY_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}
120IMPERSONATED_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}
129
130MOCK_CREDENTIALS = mock.Mock(spec=credentials.CredentialsWithQuotaProject)
131MOCK_CREDENTIALS.with_quota_project.return_value = MOCK_CREDENTIALS
132
133
134def get_project_id_side_effect(self, request=None):
135    # If no scopes are set, this will always return None.
136    if not self.scopes:
137        return None
138    return mock.sentinel.project_id
139
140
141LOAD_FILE_PATCH = mock.patch(
142    "google.auth._default.load_credentials_from_file",
143    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
144    autospec=True,
145)
146EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH = mock.patch.object(
147    external_account.Credentials,
148    "get_project_id",
149    side_effect=get_project_id_side_effect,
150    autospec=True,
151)
152
153
154def test_load_credentials_from_missing_file():
155    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
156        _default.load_credentials_from_file("")
157
158    assert excinfo.match(r"not found")
159
160
161def test_load_credentials_from_file_invalid_json(tmpdir):
162    jsonfile = tmpdir.join("invalid.json")
163    jsonfile.write("{")
164
165    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
166        _default.load_credentials_from_file(str(jsonfile))
167
168    assert excinfo.match(r"not a valid json file")
169
170
171def test_load_credentials_from_file_invalid_type(tmpdir):
172    jsonfile = tmpdir.join("invalid.json")
173    jsonfile.write(json.dumps({"type": "not-a-real-type"}))
174
175    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
176        _default.load_credentials_from_file(str(jsonfile))
177
178    assert excinfo.match(r"does not have a valid type")
179
180
181def test_load_credentials_from_file_authorized_user():
182    credentials, project_id = _default.load_credentials_from_file(AUTHORIZED_USER_FILE)
183    assert isinstance(credentials, google.oauth2.credentials.Credentials)
184    assert project_id is None
185
186
187def test_load_credentials_from_file_no_type(tmpdir):
188    # use the client_secrets.json, which is valid json but not a
189    # loadable credentials type
190    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
191        _default.load_credentials_from_file(CLIENT_SECRETS_FILE)
192
193    assert excinfo.match(r"does not have a valid type")
194    assert excinfo.match(r"Type is None")
195
196
197def test_load_credentials_from_file_authorized_user_bad_format(tmpdir):
198    filename = tmpdir.join("authorized_user_bad.json")
199    filename.write(json.dumps({"type": "authorized_user"}))
200
201    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
202        _default.load_credentials_from_file(str(filename))
203
204    assert excinfo.match(r"Failed to load authorized user")
205    assert excinfo.match(r"missing fields")
206
207
208def test_load_credentials_from_file_authorized_user_cloud_sdk():
209    with pytest.warns(UserWarning, match="Cloud SDK"):
210        credentials, project_id = _default.load_credentials_from_file(
211            AUTHORIZED_USER_CLOUD_SDK_FILE
212        )
213    assert isinstance(credentials, google.oauth2.credentials.Credentials)
214    assert project_id is None
215
216    # No warning if the json file has quota project id.
217    credentials, project_id = _default.load_credentials_from_file(
218        AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE
219    )
220    assert isinstance(credentials, google.oauth2.credentials.Credentials)
221    assert project_id is None
222
223
224def test_load_credentials_from_file_authorized_user_cloud_sdk_with_scopes():
225    with pytest.warns(UserWarning, match="Cloud SDK"):
226        credentials, project_id = _default.load_credentials_from_file(
227            AUTHORIZED_USER_CLOUD_SDK_FILE,
228            scopes=["https://www.google.com/calendar/feeds"],
229        )
230    assert isinstance(credentials, google.oauth2.credentials.Credentials)
231    assert project_id is None
232    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
233
234
235def test_load_credentials_from_file_authorized_user_cloud_sdk_with_quota_project():
236    credentials, project_id = _default.load_credentials_from_file(
237        AUTHORIZED_USER_CLOUD_SDK_FILE, quota_project_id="project-foo"
238    )
239
240    assert isinstance(credentials, google.oauth2.credentials.Credentials)
241    assert project_id is None
242    assert credentials.quota_project_id == "project-foo"
243
244
245def test_load_credentials_from_file_service_account():
246    credentials, project_id = _default.load_credentials_from_file(SERVICE_ACCOUNT_FILE)
247    assert isinstance(credentials, service_account.Credentials)
248    assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
249
250
251def test_load_credentials_from_file_service_account_with_scopes():
252    credentials, project_id = _default.load_credentials_from_file(
253        SERVICE_ACCOUNT_FILE, scopes=["https://www.google.com/calendar/feeds"]
254    )
255    assert isinstance(credentials, service_account.Credentials)
256    assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
257    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
258
259
260def test_load_credentials_from_file_service_account_with_quota_project():
261    credentials, project_id = _default.load_credentials_from_file(
262        SERVICE_ACCOUNT_FILE, quota_project_id="project-foo"
263    )
264    assert isinstance(credentials, service_account.Credentials)
265    assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
266    assert credentials.quota_project_id == "project-foo"
267
268
269def test_load_credentials_from_file_service_account_bad_format(tmpdir):
270    filename = tmpdir.join("serivce_account_bad.json")
271    filename.write(json.dumps({"type": "service_account"}))
272
273    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
274        _default.load_credentials_from_file(str(filename))
275
276    assert excinfo.match(r"Failed to load service account")
277    assert excinfo.match(r"missing fields")
278
279
280@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
281def test_load_credentials_from_file_external_account_identity_pool(
282    get_project_id, tmpdir
283):
284    config_file = tmpdir.join("config.json")
285    config_file.write(json.dumps(IDENTITY_POOL_DATA))
286    credentials, project_id = _default.load_credentials_from_file(str(config_file))
287
288    assert isinstance(credentials, identity_pool.Credentials)
289    # Since no scopes are specified, the project ID cannot be determined.
290    assert project_id is None
291    assert get_project_id.called
292
293
294@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
295def test_load_credentials_from_file_external_account_aws(get_project_id, tmpdir):
296    config_file = tmpdir.join("config.json")
297    config_file.write(json.dumps(AWS_DATA))
298    credentials, project_id = _default.load_credentials_from_file(str(config_file))
299
300    assert isinstance(credentials, aws.Credentials)
301    # Since no scopes are specified, the project ID cannot be determined.
302    assert project_id is None
303    assert get_project_id.called
304
305
306@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
307def 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
323def 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
339def 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
353def 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
368@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
369def test_load_credentials_from_file_external_account_with_user_and_default_scopes(
370    get_project_id, tmpdir
371):
372    config_file = tmpdir.join("config.json")
373    config_file.write(json.dumps(IDENTITY_POOL_DATA))
374    credentials, project_id = _default.load_credentials_from_file(
375        str(config_file),
376        scopes=["https://www.google.com/calendar/feeds"],
377        default_scopes=["https://www.googleapis.com/auth/cloud-platform"],
378    )
379
380    assert isinstance(credentials, identity_pool.Credentials)
381    # Since scopes are specified, the project ID can be determined.
382    assert project_id is mock.sentinel.project_id
383    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
384    assert credentials.default_scopes == [
385        "https://www.googleapis.com/auth/cloud-platform"
386    ]
387
388
389@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
390def test_load_credentials_from_file_external_account_with_quota_project(
391    get_project_id, tmpdir
392):
393    config_file = tmpdir.join("config.json")
394    config_file.write(json.dumps(IDENTITY_POOL_DATA))
395    credentials, project_id = _default.load_credentials_from_file(
396        str(config_file), quota_project_id="project-foo"
397    )
398
399    assert isinstance(credentials, identity_pool.Credentials)
400    # Since no scopes are specified, the project ID cannot be determined.
401    assert project_id is None
402    assert credentials.quota_project_id == "project-foo"
403
404
405def test_load_credentials_from_file_external_account_bad_format(tmpdir):
406    filename = tmpdir.join("external_account_bad.json")
407    filename.write(json.dumps({"type": "external_account"}))
408
409    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
410        _default.load_credentials_from_file(str(filename))
411
412    assert excinfo.match(
413        "Failed to load external account credentials from {}".format(str(filename))
414    )
415
416
417@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
418def test_load_credentials_from_file_external_account_explicit_request(
419    get_project_id, tmpdir
420):
421    config_file = tmpdir.join("config.json")
422    config_file.write(json.dumps(IDENTITY_POOL_DATA))
423    credentials, project_id = _default.load_credentials_from_file(
424        str(config_file),
425        request=mock.sentinel.request,
426        scopes=["https://www.googleapis.com/auth/cloud-platform"],
427    )
428
429    assert isinstance(credentials, identity_pool.Credentials)
430    # Since scopes are specified, the project ID can be determined.
431    assert project_id is mock.sentinel.project_id
432    get_project_id.assert_called_with(credentials, request=mock.sentinel.request)
433
434
435@mock.patch.dict(os.environ, {}, clear=True)
436def test__get_explicit_environ_credentials_no_env():
437    assert _default._get_explicit_environ_credentials() == (None, None)
438
439
440@pytest.mark.parametrize("quota_project_id", [None, "project-foo"])
441@LOAD_FILE_PATCH
442def test__get_explicit_environ_credentials(load, quota_project_id, monkeypatch):
443    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
444
445    credentials, project_id = _default._get_explicit_environ_credentials(
446        quota_project_id=quota_project_id
447    )
448
449    assert credentials is MOCK_CREDENTIALS
450    assert project_id is mock.sentinel.project_id
451    load.assert_called_with("filename", quota_project_id=quota_project_id)
452
453
454@LOAD_FILE_PATCH
455def test__get_explicit_environ_credentials_no_project_id(load, monkeypatch):
456    load.return_value = MOCK_CREDENTIALS, None
457    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
458
459    credentials, project_id = _default._get_explicit_environ_credentials()
460
461    assert credentials is MOCK_CREDENTIALS
462    assert project_id is None
463
464
465@pytest.mark.parametrize("quota_project_id", [None, "project-foo"])
466@mock.patch(
467    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
468)
469@mock.patch("google.auth._default._get_gcloud_sdk_credentials", autospec=True)
470def test__get_explicit_environ_credentials_fallback_to_gcloud(
471    get_gcloud_creds, get_adc_path, quota_project_id, monkeypatch
472):
473    # Set explicit credentials path to cloud sdk credentials path.
474    get_adc_path.return_value = "filename"
475    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
476
477    _default._get_explicit_environ_credentials(quota_project_id=quota_project_id)
478
479    # Check we fall back to cloud sdk flow since explicit credentials path is
480    # cloud sdk credentials path
481    get_gcloud_creds.assert_called_with(quota_project_id=quota_project_id)
482
483
484@pytest.mark.parametrize("quota_project_id", [None, "project-foo"])
485@LOAD_FILE_PATCH
486@mock.patch(
487    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
488)
489def test__get_gcloud_sdk_credentials(get_adc_path, load, quota_project_id):
490    get_adc_path.return_value = SERVICE_ACCOUNT_FILE
491
492    credentials, project_id = _default._get_gcloud_sdk_credentials(
493        quota_project_id=quota_project_id
494    )
495
496    assert credentials is MOCK_CREDENTIALS
497    assert project_id is mock.sentinel.project_id
498    load.assert_called_with(SERVICE_ACCOUNT_FILE, quota_project_id=quota_project_id)
499
500
501@mock.patch(
502    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
503)
504def test__get_gcloud_sdk_credentials_non_existent(get_adc_path, tmpdir):
505    non_existent = tmpdir.join("non-existent")
506    get_adc_path.return_value = str(non_existent)
507
508    credentials, project_id = _default._get_gcloud_sdk_credentials()
509
510    assert credentials is None
511    assert project_id is None
512
513
514@mock.patch(
515    "google.auth._cloud_sdk.get_project_id",
516    return_value=mock.sentinel.project_id,
517    autospec=True,
518)
519@mock.patch("os.path.isfile", return_value=True, autospec=True)
520@LOAD_FILE_PATCH
521def test__get_gcloud_sdk_credentials_project_id(load, unused_isfile, get_project_id):
522    # Don't return a project ID from load file, make the function check
523    # the Cloud SDK project.
524    load.return_value = MOCK_CREDENTIALS, None
525
526    credentials, project_id = _default._get_gcloud_sdk_credentials()
527
528    assert credentials == MOCK_CREDENTIALS
529    assert project_id == mock.sentinel.project_id
530    assert get_project_id.called
531
532
533@mock.patch("google.auth._cloud_sdk.get_project_id", return_value=None, autospec=True)
534@mock.patch("os.path.isfile", return_value=True)
535@LOAD_FILE_PATCH
536def test__get_gcloud_sdk_credentials_no_project_id(load, unused_isfile, get_project_id):
537    # Don't return a project ID from load file, make the function check
538    # the Cloud SDK project.
539    load.return_value = MOCK_CREDENTIALS, None
540
541    credentials, project_id = _default._get_gcloud_sdk_credentials()
542
543    assert credentials == MOCK_CREDENTIALS
544    assert project_id is None
545    assert get_project_id.called
546
547
548class _AppIdentityModule(object):
549    """The interface of the App Idenity app engine module.
550    See https://cloud.google.com/appengine/docs/standard/python/refdocs\
551    /google.appengine.api.app_identity.app_identity
552    """
553
554    def get_application_id(self):
555        raise NotImplementedError()
556
557
558@pytest.fixture
559def app_identity(monkeypatch):
560    """Mocks the app_identity module for google.auth.app_engine."""
561    app_identity_module = mock.create_autospec(_AppIdentityModule, instance=True)
562    monkeypatch.setattr(app_engine, "app_identity", app_identity_module)
563    yield app_identity_module
564
565
566@mock.patch.dict(os.environ)
567def test__get_gae_credentials_gen1(app_identity):
568    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
569    app_identity.get_application_id.return_value = mock.sentinel.project
570
571    credentials, project_id = _default._get_gae_credentials()
572
573    assert isinstance(credentials, app_engine.Credentials)
574    assert project_id == mock.sentinel.project
575
576
577@mock.patch.dict(os.environ)
578def test__get_gae_credentials_gen2():
579    os.environ["GAE_RUNTIME"] = "python37"
580    credentials, project_id = _default._get_gae_credentials()
581    assert credentials is None
582    assert project_id is None
583
584
585@mock.patch.dict(os.environ)
586def test__get_gae_credentials_gen2_backwards_compat():
587    # compat helpers may copy GAE_RUNTIME to APPENGINE_RUNTIME
588    # for backwards compatibility with code that relies on it
589    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python37"
590    os.environ["GAE_RUNTIME"] = "python37"
591    credentials, project_id = _default._get_gae_credentials()
592    assert credentials is None
593    assert project_id is None
594
595
596def test__get_gae_credentials_env_unset():
597    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
598    assert "GAE_RUNTIME" not in os.environ
599    credentials, project_id = _default._get_gae_credentials()
600    assert credentials is None
601    assert project_id is None
602
603
604@mock.patch.dict(os.environ)
605def test__get_gae_credentials_no_app_engine():
606    # test both with and without LEGACY_APPENGINE_RUNTIME setting
607    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
608
609    import sys
610
611    with mock.patch.dict(sys.modules, {"google.auth.app_engine": None}):
612        credentials, project_id = _default._get_gae_credentials()
613        assert credentials is None
614        assert project_id is None
615
616        os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
617        credentials, project_id = _default._get_gae_credentials()
618        assert credentials is None
619        assert project_id is None
620
621
622@mock.patch.dict(os.environ)
623@mock.patch.object(app_engine, "app_identity", new=None)
624def test__get_gae_credentials_no_apis():
625    # test both with and without LEGACY_APPENGINE_RUNTIME setting
626    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
627
628    credentials, project_id = _default._get_gae_credentials()
629    assert credentials is None
630    assert project_id is None
631
632    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
633    credentials, project_id = _default._get_gae_credentials()
634    assert credentials is None
635    assert project_id is None
636
637
638@mock.patch(
639    "google.auth.compute_engine._metadata.ping", return_value=True, autospec=True
640)
641@mock.patch(
642    "google.auth.compute_engine._metadata.get_project_id",
643    return_value="example-project",
644    autospec=True,
645)
646def test__get_gce_credentials(unused_get, unused_ping):
647    credentials, project_id = _default._get_gce_credentials()
648
649    assert isinstance(credentials, compute_engine.Credentials)
650    assert project_id == "example-project"
651
652
653@mock.patch(
654    "google.auth.compute_engine._metadata.ping", return_value=False, autospec=True
655)
656def test__get_gce_credentials_no_ping(unused_ping):
657    credentials, project_id = _default._get_gce_credentials()
658
659    assert credentials is None
660    assert project_id is None
661
662
663@mock.patch(
664    "google.auth.compute_engine._metadata.ping", return_value=True, autospec=True
665)
666@mock.patch(
667    "google.auth.compute_engine._metadata.get_project_id",
668    side_effect=exceptions.TransportError(),
669    autospec=True,
670)
671def test__get_gce_credentials_no_project_id(unused_get, unused_ping):
672    credentials, project_id = _default._get_gce_credentials()
673
674    assert isinstance(credentials, compute_engine.Credentials)
675    assert project_id is None
676
677
678def test__get_gce_credentials_no_compute_engine():
679    import sys
680
681    with mock.patch.dict("sys.modules"):
682        sys.modules["google.auth.compute_engine"] = None
683        credentials, project_id = _default._get_gce_credentials()
684        assert credentials is None
685        assert project_id is None
686
687
688@mock.patch(
689    "google.auth.compute_engine._metadata.ping", return_value=False, autospec=True
690)
691def test__get_gce_credentials_explicit_request(ping):
692    _default._get_gce_credentials(mock.sentinel.request)
693    ping.assert_called_with(request=mock.sentinel.request)
694
695
696@mock.patch(
697    "google.auth._default._get_explicit_environ_credentials",
698    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
699    autospec=True,
700)
701def test_default_early_out(unused_get):
702    assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
703
704
705@mock.patch(
706    "google.auth._default._get_explicit_environ_credentials",
707    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
708    autospec=True,
709)
710def test_default_explict_project_id(unused_get, monkeypatch):
711    monkeypatch.setenv(environment_vars.PROJECT, "explicit-env")
712    assert _default.default() == (MOCK_CREDENTIALS, "explicit-env")
713
714
715@mock.patch(
716    "google.auth._default._get_explicit_environ_credentials",
717    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
718    autospec=True,
719)
720def test_default_explict_legacy_project_id(unused_get, monkeypatch):
721    monkeypatch.setenv(environment_vars.LEGACY_PROJECT, "explicit-env")
722    assert _default.default() == (MOCK_CREDENTIALS, "explicit-env")
723
724
725@mock.patch("logging.Logger.warning", autospec=True)
726@mock.patch(
727    "google.auth._default._get_explicit_environ_credentials",
728    return_value=(MOCK_CREDENTIALS, None),
729    autospec=True,
730)
731@mock.patch(
732    "google.auth._default._get_gcloud_sdk_credentials",
733    return_value=(MOCK_CREDENTIALS, None),
734    autospec=True,
735)
736@mock.patch(
737    "google.auth._default._get_gae_credentials",
738    return_value=(MOCK_CREDENTIALS, None),
739    autospec=True,
740)
741@mock.patch(
742    "google.auth._default._get_gce_credentials",
743    return_value=(MOCK_CREDENTIALS, None),
744    autospec=True,
745)
746def test_default_without_project_id(
747    unused_gce, unused_gae, unused_sdk, unused_explicit, logger_warning
748):
749    assert _default.default() == (MOCK_CREDENTIALS, None)
750    logger_warning.assert_called_with(mock.ANY, mock.ANY, mock.ANY)
751
752
753@mock.patch(
754    "google.auth._default._get_explicit_environ_credentials",
755    return_value=(None, None),
756    autospec=True,
757)
758@mock.patch(
759    "google.auth._default._get_gcloud_sdk_credentials",
760    return_value=(None, None),
761    autospec=True,
762)
763@mock.patch(
764    "google.auth._default._get_gae_credentials",
765    return_value=(None, None),
766    autospec=True,
767)
768@mock.patch(
769    "google.auth._default._get_gce_credentials",
770    return_value=(None, None),
771    autospec=True,
772)
773def test_default_fail(unused_gce, unused_gae, unused_sdk, unused_explicit):
774    with pytest.raises(exceptions.DefaultCredentialsError):
775        assert _default.default()
776
777
778@mock.patch(
779    "google.auth._default._get_explicit_environ_credentials",
780    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
781    autospec=True,
782)
783@mock.patch(
784    "google.auth.credentials.with_scopes_if_required",
785    return_value=MOCK_CREDENTIALS,
786    autospec=True,
787)
788def test_default_scoped(with_scopes, unused_get):
789    scopes = ["one", "two"]
790
791    credentials, project_id = _default.default(scopes=scopes)
792
793    assert credentials == with_scopes.return_value
794    assert project_id == mock.sentinel.project_id
795    with_scopes.assert_called_once_with(MOCK_CREDENTIALS, scopes, default_scopes=None)
796
797
798@mock.patch(
799    "google.auth._default._get_explicit_environ_credentials",
800    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
801    autospec=True,
802)
803def test_default_quota_project(with_quota_project):
804    credentials, project_id = _default.default(quota_project_id="project-foo")
805
806    MOCK_CREDENTIALS.with_quota_project.assert_called_once_with("project-foo")
807    assert project_id == mock.sentinel.project_id
808
809
810@mock.patch(
811    "google.auth._default._get_explicit_environ_credentials",
812    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
813    autospec=True,
814)
815def test_default_no_app_engine_compute_engine_module(unused_get):
816    """
817    google.auth.compute_engine and google.auth.app_engine are both optional
818    to allow not including them when using this package. This verifies
819    that default fails gracefully if these modules are absent
820    """
821    import sys
822
823    with mock.patch.dict("sys.modules"):
824        sys.modules["google.auth.compute_engine"] = None
825        sys.modules["google.auth.app_engine"] = None
826        assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
827
828
829@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
830def test_default_environ_external_credentials_identity_pool(
831    get_project_id, monkeypatch, tmpdir
832):
833    config_file = tmpdir.join("config.json")
834    config_file.write(json.dumps(IDENTITY_POOL_DATA))
835    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
836
837    credentials, project_id = _default.default()
838
839    assert isinstance(credentials, identity_pool.Credentials)
840    assert not credentials.is_user
841    assert not credentials.is_workforce_pool
842    # Without scopes, project ID cannot be determined.
843    assert project_id is None
844
845
846@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
847def 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
866def 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
885def 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
904def 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
922@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
923def test_default_environ_external_credentials_with_user_and_default_scopes_and_quota_project_id(
924    get_project_id, monkeypatch, tmpdir
925):
926    config_file = tmpdir.join("config.json")
927    config_file.write(json.dumps(IDENTITY_POOL_DATA))
928    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
929
930    credentials, project_id = _default.default(
931        scopes=["https://www.google.com/calendar/feeds"],
932        default_scopes=["https://www.googleapis.com/auth/cloud-platform"],
933        quota_project_id="project-foo",
934    )
935
936    assert isinstance(credentials, identity_pool.Credentials)
937    assert project_id is mock.sentinel.project_id
938    assert credentials.quota_project_id == "project-foo"
939    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
940    assert credentials.default_scopes == [
941        "https://www.googleapis.com/auth/cloud-platform"
942    ]
943
944
945@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
946def test_default_environ_external_credentials_explicit_request_with_scopes(
947    get_project_id, monkeypatch, tmpdir
948):
949    config_file = tmpdir.join("config.json")
950    config_file.write(json.dumps(IDENTITY_POOL_DATA))
951    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
952
953    credentials, project_id = _default.default(
954        request=mock.sentinel.request,
955        scopes=["https://www.googleapis.com/auth/cloud-platform"],
956    )
957
958    assert isinstance(credentials, identity_pool.Credentials)
959    assert project_id is mock.sentinel.project_id
960    # default() will initialize new credentials via with_scopes_if_required
961    # and potentially with_quota_project.
962    # As a result the caller of get_project_id() will not match the returned
963    # credentials.
964    get_project_id.assert_called_with(mock.ANY, request=mock.sentinel.request)
965
966
967def test_default_environ_external_credentials_bad_format(monkeypatch, tmpdir):
968    filename = tmpdir.join("external_account_bad.json")
969    filename.write(json.dumps({"type": "external_account"}))
970    monkeypatch.setenv(environment_vars.CREDENTIALS, str(filename))
971
972    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
973        _default.default()
974
975    assert excinfo.match(
976        "Failed to load external account credentials from {}".format(str(filename))
977    )
978
979
980@mock.patch(
981    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
982)
983def test_default_warning_without_quota_project_id_for_user_creds(get_adc_path):
984    get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE
985
986    with pytest.warns(UserWarning, match="Cloud SDK"):
987        credentials, project_id = _default.default(quota_project_id=None)
988
989
990@mock.patch(
991    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
992)
993def test_default_no_warning_with_quota_project_id_for_user_creds(get_adc_path):
994    get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE
995
996    credentials, project_id = _default.default(quota_project_id="project-foo")
997