google-auth 2.25.2__tar.gz → 2.26.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {google-auth-2.25.2/google_auth.egg-info → google-auth-2.26.0}/PKG-INFO +1 -1
- google-auth-2.26.0/google/auth/_refresh_worker.py +98 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/credentials.py +92 -5
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/external_account.py +1 -9
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/external_account_authorized_user.py +12 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/impersonated_credentials.py +4 -1
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/version.py +1 -1
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/credentials.py +8 -9
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/service_account.py +1 -9
- {google-auth-2.25.2 → google-auth-2.26.0/google_auth.egg-info}/PKG-INFO +1 -1
- {google-auth-2.25.2 → google-auth-2.26.0}/google_auth.egg-info/SOURCES.txt +2 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_credentials.py +10 -2
- google-auth-2.26.0/tests/test__refresh_worker.py +147 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_credentials.py +118 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_downscoped.py +18 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_external_account.py +10 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_external_account_authorized_user.py +31 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_impersonated_credentials.py +1 -1
- {google-auth-2.25.2 → google-auth-2.26.0}/LICENSE +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/MANIFEST.in +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/README.rst +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_cloud_sdk.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_credentials_async.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_default.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_default_async.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_exponential_backoff.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_helpers.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_jwt_async.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_oauth2client.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_service_account_info.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/api_key.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/app_engine.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/aws.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/compute_engine/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/compute_engine/_metadata.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/compute_engine/credentials.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/_cryptography_rsa.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/_helpers.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/_python_rsa.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/base.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/es256.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/rsa.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/downscoped.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/environment_vars.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/exceptions.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/iam.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/identity_pool.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/jwt.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/metrics.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/pluggable.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/_aiohttp_requests.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/_custom_tls_signer.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/_http_client.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/_mtls_helper.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/grpc.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/mtls.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/requests.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/urllib3.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_client.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_client_async.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_credentials_async.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_id_token_async.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_reauth_async.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_service_account_async.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/challenges.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/gdch_credentials.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/id_token.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/reauth.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/sts.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/utils.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google_auth.egg-info/dependency_links.txt +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google_auth.egg-info/requires.txt +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/google_auth.egg-info/top_level.txt +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/setup.cfg +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/setup.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/data/smbios_product_name +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/data/smbios_product_name_non_google +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/test__metadata.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/test_credentials.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/conftest.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/test__cryptography_rsa.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/test__python_rsa.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/test_crypt.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/test_es256.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/authorized_user.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/authorized_user_cloud_sdk.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/authorized_user_with_rapt_token.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/client_secrets.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/context_aware_metadata.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/enterprise_cert_invalid.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/enterprise_cert_valid.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/enterprise_cert_valid_provider.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/es256_privatekey.pem +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/es256_public_cert.pem +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/es256_publickey.pem +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/es256_service_account.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/external_account_authorized_user.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/external_subject_token.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/external_subject_token.txt +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/gdch_service_account.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/impersonated_service_account_authorized_user_source.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/impersonated_service_account_service_account_source.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/impersonated_service_account_with_quota_project.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/old_oauth_credentials_py3.pickle +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/other_cert.pem +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/pem_from_pkcs12.pem +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/privatekey.p12 +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/privatekey.pem +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/privatekey.pub +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/public_cert.pem +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/service_account.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/service_account_non_gdu.json +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test__client.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_challenges.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_gdch_credentials.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_id_token.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_reauth.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_service_account.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_sts.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_utils.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__cloud_sdk.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__default.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__exponential_backoff.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__helpers.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__oauth2client.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__service_account_info.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_api_key.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_app_engine.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_aws.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_exceptions.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_iam.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_identity_pool.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_jwt.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_metrics.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_packaging.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_pluggable.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/__init__.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/compliance.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test__custom_tls_signer.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test__http_client.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test__mtls_helper.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test_grpc.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test_mtls.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test_requests.py +0 -0
- {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test_urllib3.py +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright 2023 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 copy
|
|
16
|
+
import logging
|
|
17
|
+
import threading
|
|
18
|
+
|
|
19
|
+
import google.auth.exceptions as e
|
|
20
|
+
|
|
21
|
+
_LOGGER = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RefreshThreadManager:
|
|
25
|
+
"""
|
|
26
|
+
Organizes exactly one background job that refresh a token.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initializes the manager."""
|
|
31
|
+
|
|
32
|
+
self._worker = None
|
|
33
|
+
self._lock = threading.Lock() # protects access to worker threads.
|
|
34
|
+
|
|
35
|
+
def start_refresh(self, cred, request):
|
|
36
|
+
"""Starts a refresh thread for the given credentials.
|
|
37
|
+
The credentials are refreshed using the request parameter.
|
|
38
|
+
request and cred MUST not be None
|
|
39
|
+
|
|
40
|
+
Returns True if a background refresh was kicked off. False otherwise.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
cred: A credentials object.
|
|
44
|
+
request: A request object.
|
|
45
|
+
Returns:
|
|
46
|
+
bool
|
|
47
|
+
"""
|
|
48
|
+
if cred is None or request is None:
|
|
49
|
+
raise e.InvalidValue(
|
|
50
|
+
"Unable to start refresh. cred and request must be valid and instantiated objects."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
with self._lock:
|
|
54
|
+
if self._worker is not None and self._worker._error_info is not None:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
if self._worker is None or not self._worker.is_alive(): # pragma: NO COVER
|
|
58
|
+
self._worker = RefreshThread(cred=cred, request=copy.deepcopy(request))
|
|
59
|
+
self._worker.start()
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
def clear_error(self):
|
|
63
|
+
"""
|
|
64
|
+
Removes any errors that were stored from previous background refreshes.
|
|
65
|
+
"""
|
|
66
|
+
with self._lock:
|
|
67
|
+
if self._worker:
|
|
68
|
+
self._worker._error_info = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RefreshThread(threading.Thread):
|
|
72
|
+
"""
|
|
73
|
+
Thread that refreshes credentials.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, cred, request, **kwargs):
|
|
77
|
+
"""Initializes the thread.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
cred: A Credential object to refresh.
|
|
81
|
+
request: A Request object used to perform a credential refresh.
|
|
82
|
+
**kwargs: Additional keyword arguments.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
super().__init__(**kwargs)
|
|
86
|
+
self._cred = cred
|
|
87
|
+
self._request = request
|
|
88
|
+
self._error_info = None
|
|
89
|
+
|
|
90
|
+
def run(self):
|
|
91
|
+
"""
|
|
92
|
+
Perform the credential refresh.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
self._cred.refresh(self._request)
|
|
96
|
+
except Exception as err: # pragma: NO COVER
|
|
97
|
+
_LOGGER.error(f"Background refresh failed due to: {err}")
|
|
98
|
+
self._error_info = err
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
"""Interfaces for credentials."""
|
|
17
17
|
|
|
18
18
|
import abc
|
|
19
|
+
from enum import Enum
|
|
19
20
|
import os
|
|
20
21
|
|
|
21
22
|
from google.auth import _helpers, environment_vars
|
|
22
23
|
from google.auth import exceptions
|
|
23
24
|
from google.auth import metrics
|
|
25
|
+
from google.auth._refresh_worker import RefreshThreadManager
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
class Credentials(metaclass=abc.ABCMeta):
|
|
@@ -59,6 +61,9 @@ class Credentials(metaclass=abc.ABCMeta):
|
|
|
59
61
|
"""Optional[str]: The universe domain value, default is googleapis.com
|
|
60
62
|
"""
|
|
61
63
|
|
|
64
|
+
self._use_non_blocking_refresh = False
|
|
65
|
+
self._refresh_worker = RefreshThreadManager()
|
|
66
|
+
|
|
62
67
|
@property
|
|
63
68
|
def expired(self):
|
|
64
69
|
"""Checks if the credentials are expired.
|
|
@@ -66,10 +71,12 @@ class Credentials(metaclass=abc.ABCMeta):
|
|
|
66
71
|
Note that credentials can be invalid but not expired because
|
|
67
72
|
Credentials with :attr:`expiry` set to None is considered to never
|
|
68
73
|
expire.
|
|
74
|
+
|
|
75
|
+
.. deprecated:: v2.24.0
|
|
76
|
+
Prefer checking :attr:`token_state` instead.
|
|
69
77
|
"""
|
|
70
78
|
if not self.expiry:
|
|
71
79
|
return False
|
|
72
|
-
|
|
73
80
|
# Remove some threshold from expiry to err on the side of reporting
|
|
74
81
|
# expiration early so that we avoid the 401-refresh-retry loop.
|
|
75
82
|
skewed_expiry = self.expiry - _helpers.REFRESH_THRESHOLD
|
|
@@ -81,9 +88,34 @@ class Credentials(metaclass=abc.ABCMeta):
|
|
|
81
88
|
|
|
82
89
|
This is True if the credentials have a :attr:`token` and the token
|
|
83
90
|
is not :attr:`expired`.
|
|
91
|
+
|
|
92
|
+
.. deprecated:: v2.24.0
|
|
93
|
+
Prefer checking :attr:`token_state` instead.
|
|
84
94
|
"""
|
|
85
95
|
return self.token is not None and not self.expired
|
|
86
96
|
|
|
97
|
+
@property
|
|
98
|
+
def token_state(self):
|
|
99
|
+
"""
|
|
100
|
+
See `:obj:`TokenState`
|
|
101
|
+
"""
|
|
102
|
+
if self.token is None:
|
|
103
|
+
return TokenState.INVALID
|
|
104
|
+
|
|
105
|
+
# Credentials that can't expire are always treated as fresh.
|
|
106
|
+
if self.expiry is None:
|
|
107
|
+
return TokenState.FRESH
|
|
108
|
+
|
|
109
|
+
expired = _helpers.utcnow() >= self.expiry
|
|
110
|
+
if expired:
|
|
111
|
+
return TokenState.INVALID
|
|
112
|
+
|
|
113
|
+
is_stale = _helpers.utcnow() >= (self.expiry - _helpers.REFRESH_THRESHOLD)
|
|
114
|
+
if is_stale:
|
|
115
|
+
return TokenState.STALE
|
|
116
|
+
|
|
117
|
+
return TokenState.FRESH
|
|
118
|
+
|
|
87
119
|
@property
|
|
88
120
|
def quota_project_id(self):
|
|
89
121
|
"""Project to use for quota and billing purposes."""
|
|
@@ -154,6 +186,25 @@ class Credentials(metaclass=abc.ABCMeta):
|
|
|
154
186
|
if self.quota_project_id:
|
|
155
187
|
headers["x-goog-user-project"] = self.quota_project_id
|
|
156
188
|
|
|
189
|
+
def _blocking_refresh(self, request):
|
|
190
|
+
if not self.valid:
|
|
191
|
+
self.refresh(request)
|
|
192
|
+
|
|
193
|
+
def _non_blocking_refresh(self, request):
|
|
194
|
+
use_blocking_refresh_fallback = False
|
|
195
|
+
|
|
196
|
+
if self.token_state == TokenState.STALE:
|
|
197
|
+
use_blocking_refresh_fallback = not self._refresh_worker.start_refresh(
|
|
198
|
+
self, request
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if self.token_state == TokenState.INVALID or use_blocking_refresh_fallback:
|
|
202
|
+
self.refresh(request)
|
|
203
|
+
# If the blocking refresh succeeds then we can clear the error info
|
|
204
|
+
# on the background refresh worker, and perform refreshes in a
|
|
205
|
+
# background thread.
|
|
206
|
+
self._refresh_worker.clear_error()
|
|
207
|
+
|
|
157
208
|
def before_request(self, request, method, url, headers):
|
|
158
209
|
"""Performs credential-specific before request logic.
|
|
159
210
|
|
|
@@ -171,11 +222,17 @@ class Credentials(metaclass=abc.ABCMeta):
|
|
|
171
222
|
# pylint: disable=unused-argument
|
|
172
223
|
# (Subclasses may use these arguments to ascertain information about
|
|
173
224
|
# the http request.)
|
|
174
|
-
if
|
|
175
|
-
self.
|
|
225
|
+
if self._use_non_blocking_refresh:
|
|
226
|
+
self._non_blocking_refresh(request)
|
|
227
|
+
else:
|
|
228
|
+
self._blocking_refresh(request)
|
|
229
|
+
|
|
176
230
|
metrics.add_metric_header(headers, self._metric_header_for_usage())
|
|
177
231
|
self.apply(headers)
|
|
178
232
|
|
|
233
|
+
def with_non_blocking_refresh(self):
|
|
234
|
+
self._use_non_blocking_refresh = True
|
|
235
|
+
|
|
179
236
|
|
|
180
237
|
class CredentialsWithQuotaProject(Credentials):
|
|
181
238
|
"""Abstract base for credentials supporting ``with_quota_project`` factory"""
|
|
@@ -188,7 +245,7 @@ class CredentialsWithQuotaProject(Credentials):
|
|
|
188
245
|
billing purposes
|
|
189
246
|
|
|
190
247
|
Returns:
|
|
191
|
-
google.
|
|
248
|
+
google.auth.credentials.Credentials: A new credentials instance.
|
|
192
249
|
"""
|
|
193
250
|
raise NotImplementedError("This credential does not support quota project.")
|
|
194
251
|
|
|
@@ -209,11 +266,28 @@ class CredentialsWithTokenUri(Credentials):
|
|
|
209
266
|
token_uri (str): The uri to use for fetching/exchanging tokens
|
|
210
267
|
|
|
211
268
|
Returns:
|
|
212
|
-
google.
|
|
269
|
+
google.auth.credentials.Credentials: A new credentials instance.
|
|
213
270
|
"""
|
|
214
271
|
raise NotImplementedError("This credential does not use token uri.")
|
|
215
272
|
|
|
216
273
|
|
|
274
|
+
class CredentialsWithUniverseDomain(Credentials):
|
|
275
|
+
"""Abstract base for credentials supporting ``with_universe_domain`` factory"""
|
|
276
|
+
|
|
277
|
+
def with_universe_domain(self, universe_domain):
|
|
278
|
+
"""Returns a copy of these credentials with a modified universe domain.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
universe_domain (str): The universe domain to use
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
google.auth.credentials.Credentials: A new credentials instance.
|
|
285
|
+
"""
|
|
286
|
+
raise NotImplementedError(
|
|
287
|
+
"This credential does not support with_universe_domain."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
217
291
|
class AnonymousCredentials(Credentials):
|
|
218
292
|
"""Credentials that do not provide any authentication information.
|
|
219
293
|
|
|
@@ -422,3 +496,16 @@ class Signing(metaclass=abc.ABCMeta):
|
|
|
422
496
|
# pylint: disable=missing-raises-doc
|
|
423
497
|
# (pylint doesn't recognize that this is abstract)
|
|
424
498
|
raise NotImplementedError("Signer must be implemented.")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class TokenState(Enum):
|
|
502
|
+
"""
|
|
503
|
+
Tracks the state of a token.
|
|
504
|
+
FRESH: The token is valid. It is not expired or close to expired, or the token has no expiry.
|
|
505
|
+
STALE: The token is close to expired, and should be refreshed. The token can be used normally.
|
|
506
|
+
INVALID: The token is expired or invalid. The token cannot be used for a normal operation.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
FRESH = 1
|
|
510
|
+
STALE = 2
|
|
511
|
+
INVALID = 3
|
|
@@ -415,16 +415,8 @@ class Credentials(
|
|
|
415
415
|
new_cred._metrics_options = self._metrics_options
|
|
416
416
|
return new_cred
|
|
417
417
|
|
|
418
|
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
|
418
419
|
def with_universe_domain(self, universe_domain):
|
|
419
|
-
"""Create a copy of these credentials with the given universe domain.
|
|
420
|
-
|
|
421
|
-
Args:
|
|
422
|
-
universe_domain (str): The universe domain value.
|
|
423
|
-
|
|
424
|
-
Returns:
|
|
425
|
-
google.auth.external_account.Credentials: A new credentials
|
|
426
|
-
instance.
|
|
427
|
-
"""
|
|
428
420
|
kwargs = self._constructor_args()
|
|
429
421
|
kwargs.update(universe_domain=universe_domain)
|
|
430
422
|
new_cred = self.__class__(**kwargs)
|
|
@@ -43,6 +43,7 @@ from google.auth import exceptions
|
|
|
43
43
|
from google.oauth2 import sts
|
|
44
44
|
from google.oauth2 import utils
|
|
45
45
|
|
|
46
|
+
_DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
|
|
46
47
|
_EXTERNAL_ACCOUNT_AUTHORIZED_USER_JSON_TYPE = "external_account_authorized_user"
|
|
47
48
|
|
|
48
49
|
|
|
@@ -75,6 +76,7 @@ class Credentials(
|
|
|
75
76
|
revoke_url=None,
|
|
76
77
|
scopes=None,
|
|
77
78
|
quota_project_id=None,
|
|
79
|
+
universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
|
|
78
80
|
):
|
|
79
81
|
"""Instantiates a external account authorized user credentials object.
|
|
80
82
|
|
|
@@ -98,6 +100,8 @@ class Credentials(
|
|
|
98
100
|
quota_project_id (str): The optional project ID used for quota and billing.
|
|
99
101
|
This project may be different from the project used to
|
|
100
102
|
create the credentials.
|
|
103
|
+
universe_domain (Optional[str]): The universe domain. The default value
|
|
104
|
+
is googleapis.com.
|
|
101
105
|
|
|
102
106
|
Returns:
|
|
103
107
|
google.auth.external_account_authorized_user.Credentials: The
|
|
@@ -116,6 +120,7 @@ class Credentials(
|
|
|
116
120
|
self._revoke_url = revoke_url
|
|
117
121
|
self._quota_project_id = quota_project_id
|
|
118
122
|
self._scopes = scopes
|
|
123
|
+
self._universe_domain = universe_domain or _DEFAULT_UNIVERSE_DOMAIN
|
|
119
124
|
|
|
120
125
|
if not self.valid and not self.can_refresh:
|
|
121
126
|
raise exceptions.InvalidOperation(
|
|
@@ -162,6 +167,7 @@ class Credentials(
|
|
|
162
167
|
"revoke_url": self._revoke_url,
|
|
163
168
|
"scopes": self._scopes,
|
|
164
169
|
"quota_project_id": self._quota_project_id,
|
|
170
|
+
"universe_domain": self._universe_domain,
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
@property
|
|
@@ -297,6 +303,12 @@ class Credentials(
|
|
|
297
303
|
kwargs.update(token_url=token_uri)
|
|
298
304
|
return self.__class__(**kwargs)
|
|
299
305
|
|
|
306
|
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
|
307
|
+
def with_universe_domain(self, universe_domain):
|
|
308
|
+
kwargs = self.constructor_args()
|
|
309
|
+
kwargs.update(universe_domain=universe_domain)
|
|
310
|
+
return self.__class__(**kwargs)
|
|
311
|
+
|
|
300
312
|
@classmethod
|
|
301
313
|
def from_info(cls, info, **kwargs):
|
|
302
314
|
"""Creates a Credentials instance from parsed external account info.
|
|
@@ -259,7 +259,10 @@ class Credentials(
|
|
|
259
259
|
"""
|
|
260
260
|
|
|
261
261
|
# Refresh our source credentials if it is not valid.
|
|
262
|
-
if
|
|
262
|
+
if (
|
|
263
|
+
self._source_credentials.token_state == credentials.TokenState.STALE
|
|
264
|
+
or self._source_credentials.token_state == credentials.TokenState.INVALID
|
|
265
|
+
):
|
|
263
266
|
self._source_credentials.refresh(request)
|
|
264
267
|
|
|
265
268
|
body = {
|
|
@@ -160,7 +160,11 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
|
|
|
160
160
|
# unpickling certain callables (lambda, functools.partial instances)
|
|
161
161
|
# because they need to be importable.
|
|
162
162
|
# Instead, the refresh_handler setter should be used to repopulate this.
|
|
163
|
-
|
|
163
|
+
if "_refresh_handler" in state_dict:
|
|
164
|
+
del state_dict["_refresh_handler"]
|
|
165
|
+
|
|
166
|
+
if "_refresh_worker" in state_dict:
|
|
167
|
+
del state_dict["_refresh_worker"]
|
|
164
168
|
return state_dict
|
|
165
169
|
|
|
166
170
|
def __setstate__(self, d):
|
|
@@ -183,6 +187,8 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
|
|
|
183
187
|
self._universe_domain = d.get("_universe_domain") or _DEFAULT_UNIVERSE_DOMAIN
|
|
184
188
|
# The refresh_handler setter should be used to repopulate this.
|
|
185
189
|
self._refresh_handler = None
|
|
190
|
+
self._refresh_worker = None
|
|
191
|
+
self._use_non_blocking_refresh = d.get("_use_non_blocking_refresh", False)
|
|
186
192
|
|
|
187
193
|
@property
|
|
188
194
|
def refresh_token(self):
|
|
@@ -302,15 +308,8 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
|
|
|
302
308
|
universe_domain=self._universe_domain,
|
|
303
309
|
)
|
|
304
310
|
|
|
311
|
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
|
305
312
|
def with_universe_domain(self, universe_domain):
|
|
306
|
-
"""Create a copy of the credential with the given universe domain.
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
universe_domain (str): The universe domain value.
|
|
310
|
-
|
|
311
|
-
Returns:
|
|
312
|
-
google.oauth2.credentials.Credentials: A new credentials instance.
|
|
313
|
-
"""
|
|
314
313
|
|
|
315
314
|
return self.__class__(
|
|
316
315
|
self.token,
|
|
@@ -325,16 +325,8 @@ class Credentials(
|
|
|
325
325
|
cred._always_use_jwt_access = always_use_jwt_access
|
|
326
326
|
return cred
|
|
327
327
|
|
|
328
|
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
|
328
329
|
def with_universe_domain(self, universe_domain):
|
|
329
|
-
"""Create a copy of these credentials with the given universe domain.
|
|
330
|
-
|
|
331
|
-
Args:
|
|
332
|
-
universe_domain (str): The universe domain value.
|
|
333
|
-
|
|
334
|
-
Returns:
|
|
335
|
-
google.auth.service_account.Credentials: A new credentials
|
|
336
|
-
instance.
|
|
337
|
-
"""
|
|
338
330
|
cred = self._make_copy()
|
|
339
331
|
cred._universe_domain = universe_domain
|
|
340
332
|
if universe_domain != _DEFAULT_UNIVERSE_DOMAIN:
|
|
@@ -12,6 +12,7 @@ google/auth/_exponential_backoff.py
|
|
|
12
12
|
google/auth/_helpers.py
|
|
13
13
|
google/auth/_jwt_async.py
|
|
14
14
|
google/auth/_oauth2client.py
|
|
15
|
+
google/auth/_refresh_worker.py
|
|
15
16
|
google/auth/_service_account_info.py
|
|
16
17
|
google/auth/api_key.py
|
|
17
18
|
google/auth/app_engine.py
|
|
@@ -75,6 +76,7 @@ tests/test__default.py
|
|
|
75
76
|
tests/test__exponential_backoff.py
|
|
76
77
|
tests/test__helpers.py
|
|
77
78
|
tests/test__oauth2client.py
|
|
79
|
+
tests/test__refresh_worker.py
|
|
78
80
|
tests/test__service_account_info.py
|
|
79
81
|
tests/test_api_key.py
|
|
80
82
|
tests/test_app_engine.py
|
|
@@ -24,6 +24,7 @@ import pytest # type: ignore
|
|
|
24
24
|
from google.auth import _helpers
|
|
25
25
|
from google.auth import exceptions
|
|
26
26
|
from google.auth import transport
|
|
27
|
+
from google.auth.credentials import TokenState
|
|
27
28
|
from google.oauth2 import credentials
|
|
28
29
|
|
|
29
30
|
|
|
@@ -61,6 +62,7 @@ class TestCredentials(object):
|
|
|
61
62
|
assert not credentials.expired
|
|
62
63
|
# Scopes aren't required for these credentials
|
|
63
64
|
assert not credentials.requires_scopes
|
|
65
|
+
assert credentials.token_state == TokenState.INVALID
|
|
64
66
|
# Test properties
|
|
65
67
|
assert credentials.refresh_token == self.REFRESH_TOKEN
|
|
66
68
|
assert credentials.token_uri == self.TOKEN_URI
|
|
@@ -911,7 +913,11 @@ class TestCredentials(object):
|
|
|
911
913
|
assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort()
|
|
912
914
|
|
|
913
915
|
for attr in list(creds.__dict__):
|
|
914
|
-
|
|
916
|
+
# Worker should always be None
|
|
917
|
+
if attr == "_refresh_worker":
|
|
918
|
+
assert getattr(unpickled, attr) is None
|
|
919
|
+
else:
|
|
920
|
+
assert getattr(creds, attr) == getattr(unpickled, attr)
|
|
915
921
|
|
|
916
922
|
def test_pickle_and_unpickle_universe_domain(self):
|
|
917
923
|
# old version of auth lib doesn't have _universe_domain, so the pickled
|
|
@@ -945,7 +951,7 @@ class TestCredentials(object):
|
|
|
945
951
|
for attr in list(creds.__dict__):
|
|
946
952
|
# For the _refresh_handler property, the unpickled creds should be
|
|
947
953
|
# set to None.
|
|
948
|
-
if attr == "_refresh_handler":
|
|
954
|
+
if attr == "_refresh_handler" or attr == "_refresh_worker":
|
|
949
955
|
assert getattr(unpickled, attr) is None
|
|
950
956
|
else:
|
|
951
957
|
assert getattr(creds, attr) == getattr(unpickled, attr)
|
|
@@ -957,6 +963,8 @@ class TestCredentials(object):
|
|
|
957
963
|
# this mimics a pickle created with a previous class definition with
|
|
958
964
|
# fewer attributes
|
|
959
965
|
del creds.__dict__["_quota_project_id"]
|
|
966
|
+
del creds.__dict__["_refresh_handler"]
|
|
967
|
+
del creds.__dict__["_refresh_worker"]
|
|
960
968
|
|
|
961
969
|
unpickled = pickle.loads(pickle.dumps(creds))
|
|
962
970
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Copyright 2023 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 random
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
import mock
|
|
20
|
+
import pytest # type: ignore
|
|
21
|
+
|
|
22
|
+
from google.auth import _refresh_worker, credentials, exceptions
|
|
23
|
+
|
|
24
|
+
MAIN_THREAD_SLEEP_MS = 100 / 1000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MockCredentialsImpl(credentials.Credentials):
|
|
28
|
+
def __init__(self, sleep_seconds=None):
|
|
29
|
+
self.refresh_count = 0
|
|
30
|
+
self.token = None
|
|
31
|
+
self.sleep_seconds = sleep_seconds if sleep_seconds else None
|
|
32
|
+
|
|
33
|
+
def refresh(self, request):
|
|
34
|
+
if self.sleep_seconds:
|
|
35
|
+
time.sleep(self.sleep_seconds)
|
|
36
|
+
self.token = request
|
|
37
|
+
self.refresh_count += 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def test_thread_count():
|
|
42
|
+
return 25
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _cred_spinlock(cred):
|
|
46
|
+
while cred.token is None: # pragma: NO COVER
|
|
47
|
+
time.sleep(MAIN_THREAD_SLEEP_MS)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_invalid_start_refresh():
|
|
51
|
+
w = _refresh_worker.RefreshThreadManager()
|
|
52
|
+
with pytest.raises(exceptions.InvalidValue):
|
|
53
|
+
w.start_refresh(None, None)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_start_refresh():
|
|
57
|
+
w = _refresh_worker.RefreshThreadManager()
|
|
58
|
+
cred = MockCredentialsImpl()
|
|
59
|
+
request = mock.MagicMock()
|
|
60
|
+
assert w.start_refresh(cred, request)
|
|
61
|
+
|
|
62
|
+
assert w._worker is not None
|
|
63
|
+
|
|
64
|
+
_cred_spinlock(cred)
|
|
65
|
+
|
|
66
|
+
assert cred.token == request
|
|
67
|
+
assert cred.refresh_count == 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_nonblocking_start_refresh():
|
|
71
|
+
w = _refresh_worker.RefreshThreadManager()
|
|
72
|
+
cred = MockCredentialsImpl(sleep_seconds=1)
|
|
73
|
+
request = mock.MagicMock()
|
|
74
|
+
assert w.start_refresh(cred, request)
|
|
75
|
+
|
|
76
|
+
assert w._worker is not None
|
|
77
|
+
assert not cred.token
|
|
78
|
+
assert cred.refresh_count == 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_multiple_refreshes_multiple_workers(test_thread_count):
|
|
82
|
+
w = _refresh_worker.RefreshThreadManager()
|
|
83
|
+
cred = MockCredentialsImpl()
|
|
84
|
+
request = mock.MagicMock()
|
|
85
|
+
|
|
86
|
+
def _thread_refresh():
|
|
87
|
+
time.sleep(random.randrange(0, 5))
|
|
88
|
+
assert w.start_refresh(cred, request)
|
|
89
|
+
|
|
90
|
+
threads = [
|
|
91
|
+
threading.Thread(target=_thread_refresh) for _ in range(test_thread_count)
|
|
92
|
+
]
|
|
93
|
+
for t in threads:
|
|
94
|
+
t.start()
|
|
95
|
+
|
|
96
|
+
_cred_spinlock(cred)
|
|
97
|
+
|
|
98
|
+
assert cred.token == request
|
|
99
|
+
# There is a chance only one thread has enough time to perform a refresh.
|
|
100
|
+
# Generally multiple threads will have time to perform a refresh
|
|
101
|
+
assert cred.refresh_count > 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_refresh_error():
|
|
105
|
+
w = _refresh_worker.RefreshThreadManager()
|
|
106
|
+
cred = mock.MagicMock()
|
|
107
|
+
request = mock.MagicMock()
|
|
108
|
+
|
|
109
|
+
cred.refresh.side_effect = exceptions.RefreshError("Failed to refresh")
|
|
110
|
+
|
|
111
|
+
assert w.start_refresh(cred, request)
|
|
112
|
+
|
|
113
|
+
while w._worker._error_info is None: # pragma: NO COVER
|
|
114
|
+
time.sleep(MAIN_THREAD_SLEEP_MS)
|
|
115
|
+
|
|
116
|
+
assert w._worker is not None
|
|
117
|
+
assert isinstance(w._worker._error_info, exceptions.RefreshError)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_refresh_error_call_refresh_again():
|
|
121
|
+
w = _refresh_worker.RefreshThreadManager()
|
|
122
|
+
cred = mock.MagicMock()
|
|
123
|
+
request = mock.MagicMock()
|
|
124
|
+
|
|
125
|
+
cred.refresh.side_effect = exceptions.RefreshError("Failed to refresh")
|
|
126
|
+
|
|
127
|
+
assert w.start_refresh(cred, request)
|
|
128
|
+
|
|
129
|
+
while w._worker._error_info is None: # pragma: NO COVER
|
|
130
|
+
time.sleep(MAIN_THREAD_SLEEP_MS)
|
|
131
|
+
|
|
132
|
+
assert not w.start_refresh(cred, request)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_refresh_dead_worker():
|
|
136
|
+
cred = MockCredentialsImpl()
|
|
137
|
+
request = mock.MagicMock()
|
|
138
|
+
|
|
139
|
+
w = _refresh_worker.RefreshThreadManager()
|
|
140
|
+
w._worker = None
|
|
141
|
+
|
|
142
|
+
w.start_refresh(cred, request)
|
|
143
|
+
|
|
144
|
+
_cred_spinlock(cred)
|
|
145
|
+
|
|
146
|
+
assert cred.token == request
|
|
147
|
+
assert cred.refresh_count == 1
|