Skip to content

Commit 9ee9410

Browse files
refactor: export CertRotationInterceptor natively in google-auth
1 parent c4d05ff commit 9ee9410

2 files changed

Lines changed: 54 additions & 41 deletions

File tree

packages/google-auth/google/auth/transport/grpc.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -324,9 +324,9 @@ def my_client_cert_callback():
324324
"_is_recreation": True, # Hidden flag to stop recursion
325325
**kwargs,
326326
}
327-
interceptor = _MTLSCallInterceptor()
327+
interceptor = CertRotationInterceptor()
328328

329-
wrapper = _MTLSRefreshingChannel(target, factory_args, channel, cached_cert)
329+
wrapper = MTLSRefreshingChannel(target, factory_args, channel, cached_cert)
330330

331331
interceptor._wrapper = wrapper
332332
return grpc.intercept_channel(wrapper, interceptor)
@@ -401,7 +401,7 @@ def is_mtls(self):
401401
return self._is_mtls
402402

403403

404-
class _MTLSCallInterceptor(
404+
class CertRotationInterceptor(
405405
grpc.UnaryUnaryClientInterceptor,
406406
grpc.UnaryStreamClientInterceptor,
407407
grpc.StreamUnaryClientInterceptor,
@@ -413,7 +413,7 @@ class _MTLSCallInterceptor(
413413
futures or iterators. Its primary role is to monitor responses for `UNAUTHENTICATED`
414414
errors. When an authentication failure occurs, it uses `_should_retry()` to check
415415
if a new mTLS certificate is available. If a new certificate is found, it signals
416-
its associated `_MTLSRefreshingChannel` wrapper to refresh the underlying gRPC
416+
its associated `MTLSRefreshingChannel` wrapper to refresh the underlying gRPC
417417
channel's credentials and automatically replays the failed RPC.
418418
"""
419419

@@ -485,7 +485,7 @@ def intercept_stream_stream(
485485
)
486486

487487

488-
class _MTLSRefreshingChannel(grpc.Channel):
488+
class MTLSRefreshingChannel(grpc.Channel):
489489
def __init__(self, target, factory_args, initial_channel, initial_cert):
490490
self._target = target
491491
self._factory_args = factory_args
@@ -681,6 +681,7 @@ def __init__(
681681
self._completion_event = threading.Event()
682682
self._done_callbacks = []
683683
self._terminal_exception = None
684+
self._attempt_cert = None
684685

685686
self._start_call()
686687

@@ -734,7 +735,18 @@ def _on_inner_future_done(self, inner_future):
734735
_LOGGER.warning("Callback failed: %s", e)
735736
return
736737

737-
exc = inner_future.exception()
738+
try:
739+
exc = inner_future.exception()
740+
except Exception: # gracefully handle CancelledError or other future errors
741+
with self._lock:
742+
self._completion_event.set()
743+
callbacks_to_fire = list(self._done_callbacks)
744+
for fn in callbacks_to_fire:
745+
try:
746+
fn(self)
747+
except Exception as e:
748+
_LOGGER.warning("Callback failed: %s", e)
749+
return
738750
if isinstance(exc, grpc.RpcError):
739751
status_code = exc.code()
740752

@@ -745,10 +757,10 @@ def _on_inner_future_done(self, inner_future):
745757
)
746758

747759
should_retry, call_cert, call_key, pwd = self._interceptor._should_retry(
748-
status_code, self._retry_count, getattr(self, "_attempt_cert", None)
760+
status_code, self._retry_count, self._attempt_cert
749761
)
750762
if can_replay and should_retry:
751-
if getattr(self._interceptor, "_wrapper", None):
763+
if self._interceptor._wrapper:
752764
try:
753765
self._interceptor._wrapper.refresh_logic(
754766
1, call_cert, call_key, pwd
@@ -922,13 +934,14 @@ def __init__(
922934

923935
self._is_completed = False
924936
self._done_callbacks = []
937+
self._attempt_cert = None
925938

926939
self._start_call()
927940

928941
def _start_call(self):
929942
self._attempt_cert = (
930943
self._interceptor._wrapper._cached_cert
931-
if getattr(self._interceptor, "_wrapper", None)
944+
if self._interceptor._wrapper
932945
else None
933946
)
934947
with self._lock:
@@ -1022,12 +1035,12 @@ def __next__(self):
10221035
) = self._interceptor._should_retry(
10231036
status_code,
10241037
self._retry_count,
1025-
getattr(self, "_attempt_cert", None),
1038+
self._attempt_cert,
10261039
)
10271040

10281041
if not self._yielded_any_response and can_replay and should_retry:
10291042
try:
1030-
if getattr(self._interceptor, "_wrapper", None):
1043+
if self._interceptor._wrapper:
10311044
self._interceptor._wrapper.refresh_logic(
10321045
1, call_cert, call_key, pwd
10331046
)
@@ -1043,14 +1056,14 @@ def __next__(self):
10431056
continue
10441057
else:
10451058
# Non-retryable error, check if another rotation happened while we were finishing
1046-
if getattr(self._interceptor, "_wrapper", None):
1059+
if self._interceptor._wrapper:
10471060
(
10481061
chk_should_retry,
10491062
chk_cert,
10501063
chk_key,
10511064
chk_pwd,
10521065
) = self._interceptor._should_retry(
1053-
status_code, 0, getattr(self, "_attempt_cert", None)
1066+
status_code, 0, self._attempt_cert
10541067
)
10551068
if chk_should_retry:
10561069
try:
@@ -1065,7 +1078,7 @@ def __next__(self):
10651078

10661079
def add_done_callback(self, fn):
10671080
with self._lock:
1068-
if getattr(self, "_is_completed", False):
1081+
if self._is_completed:
10691082
fire_now = True
10701083
else:
10711084
self._done_callbacks.append(fn)

packages/google-auth/tests/transport/test_grpc.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ def test_get_client_ssl_credentials_auto_enablement(
696696
def test_interceptor_uses_factory_if_callable(mock_replayable):
697697
import google.auth.transport.grpc as transport_grpc
698698

699-
interceptor = transport_grpc._MTLSCallInterceptor()
699+
interceptor = transport_grpc.CertRotationInterceptor()
700700

701701
call_no_factory = transport_grpc._RetryableStreamResponseIterator(
702702
continuation=mock.Mock(),
@@ -722,11 +722,11 @@ def generator_factory():
722722
assert call_factory._payload is None
723723

724724

725-
@mock.patch("google.auth.transport.grpc._MTLSCallInterceptor._should_retry")
725+
@mock.patch("google.auth.transport.grpc.CertRotationInterceptor._should_retry")
726726
def test_factory_infinite_replay_on_error(mock_should_retry):
727727
import google.auth.transport.grpc as transport_grpc
728728

729-
interceptor = transport_grpc._MTLSCallInterceptor()
729+
interceptor = transport_grpc.CertRotationInterceptor()
730730
interceptor._wrapper = mock.Mock()
731731
interceptor._wrapper._cached_cert = "cert"
732732
mock_should_retry.side_effect = [
@@ -782,7 +782,7 @@ def test_refresh_logic_closes_old_channel(
782782

783783
subscriber = mock.Mock()
784784

785-
refreshing_channel = transport_grpc._MTLSRefreshingChannel(
785+
refreshing_channel = transport_grpc.MTLSRefreshingChannel(
786786
target="example.com:443",
787787
factory_args={},
788788
initial_channel=old_channel,
@@ -799,11 +799,11 @@ def test_refresh_logic_closes_old_channel(
799799
# old_channel.close.assert_called_once() # Removed in PR 18019
800800

801801

802-
@mock.patch("google.auth.transport.grpc._MTLSCallInterceptor._should_retry")
802+
@mock.patch("google.auth.transport.grpc.CertRotationInterceptor._should_retry")
803803
def test_unary_response_future_deadline_exceeded_on_retry(mock_should_retry):
804804
import google.auth.transport.grpc as transport_grpc
805805

806-
interceptor = transport_grpc._MTLSCallInterceptor()
806+
interceptor = transport_grpc.CertRotationInterceptor()
807807
interceptor._wrapper = mock.Mock()
808808
interceptor._wrapper._cached_cert = "cert"
809809
mock_should_retry.return_value = (True, b"cert", b"key", None)
@@ -847,11 +847,11 @@ def callback(f):
847847
future.result(timeout=1)
848848

849849

850-
@mock.patch("google.auth.transport.grpc._MTLSCallInterceptor._should_retry")
850+
@mock.patch("google.auth.transport.grpc.CertRotationInterceptor._should_retry")
851851
def test_unary_response_future_cancelled(mock_should_retry):
852852
import google.auth.transport.grpc as transport_grpc
853853

854-
interceptor = transport_grpc._MTLSCallInterceptor()
854+
interceptor = transport_grpc.CertRotationInterceptor()
855855
interceptor._wrapper = mock.Mock()
856856
interceptor._wrapper._cached_cert = "cert"
857857

@@ -886,11 +886,11 @@ def failing_callback(f):
886886
assert len(callbacks_fired) == 1
887887

888888

889-
@mock.patch("google.auth.transport.grpc._MTLSCallInterceptor._should_retry")
889+
@mock.patch("google.auth.transport.grpc.CertRotationInterceptor._should_retry")
890890
def test_unary_response_future_rpc_error_retry_start_call_exception(mock_should_retry):
891891
import google.auth.transport.grpc as transport_grpc
892892

893-
interceptor = transport_grpc._MTLSCallInterceptor()
893+
interceptor = transport_grpc.CertRotationInterceptor()
894894
interceptor._wrapper = mock.Mock()
895895
interceptor._wrapper._cached_cert = "cert"
896896

@@ -943,7 +943,7 @@ def test_start_call_wrapper_none():
943943
import pytest
944944
import google.auth.transport.grpc as transport_grpc
945945

946-
interceptor = transport_grpc._MTLSCallInterceptor()
946+
interceptor = transport_grpc.CertRotationInterceptor()
947947
if hasattr(interceptor, "_wrapper"):
948948
del interceptor._wrapper
949949

@@ -963,7 +963,7 @@ def test_start_call_wrapper_none():
963963
def test_start_call_wrapper_none_branch():
964964
import google.auth.transport.grpc as transport_grpc
965965

966-
interceptor = transport_grpc._MTLSCallInterceptor()
966+
interceptor = transport_grpc.CertRotationInterceptor()
967967
interceptor._wrapper = None
968968

969969
inner_future = mock.Mock()
@@ -980,11 +980,11 @@ def test_start_call_wrapper_none_branch():
980980
assert getattr(future, "_attempt_cert", "NOT_SET") is None
981981

982982

983-
@mock.patch("google.auth.transport.grpc._MTLSCallInterceptor._should_retry")
983+
@mock.patch("google.auth.transport.grpc.CertRotationInterceptor._should_retry")
984984
def test_unary_response_future_rpc_error_no_wrapper(mock_should_retry):
985985
import google.auth.transport.grpc as transport_grpc
986986

987-
interceptor = transport_grpc._MTLSCallInterceptor()
987+
interceptor = transport_grpc.CertRotationInterceptor()
988988
interceptor._wrapper = None
989989

990990
mock_err = transport_grpc.grpc.RpcError()
@@ -1010,11 +1010,11 @@ def test_unary_response_future_rpc_error_no_wrapper(mock_should_retry):
10101010
future._on_inner_future_done(inner_future)
10111011

10121012

1013-
@mock.patch("google.auth.transport.grpc._MTLSCallInterceptor._should_retry")
1013+
@mock.patch("google.auth.transport.grpc.CertRotationInterceptor._should_retry")
10141014
def test_unary_response_future_rpc_error_should_not_retry(mock_should_retry):
10151015
import google.auth.transport.grpc as transport_grpc
10161016

1017-
interceptor = transport_grpc._MTLSCallInterceptor()
1017+
interceptor = transport_grpc.CertRotationInterceptor()
10181018
interceptor._wrapper = mock.Mock()
10191019
interceptor._wrapper._cached_cert = "cert"
10201020

@@ -1047,7 +1047,7 @@ def test_mtls_call_interceptor_should_retry_cases():
10471047
from unittest import mock
10481048
import google.auth.transport.grpc as transport_grpc
10491049

1050-
interceptor = transport_grpc._MTLSCallInterceptor()
1050+
interceptor = transport_grpc.CertRotationInterceptor()
10511051

10521052
assert interceptor._should_retry(grpc.StatusCode.UNAUTHENTICATED, 0, "cert1") == (
10531053
False,
@@ -1102,7 +1102,7 @@ def test_mtls_call_interceptor_interceptors_methods():
11021102
from unittest import mock
11031103
import google.auth.transport.grpc as transport_grpc
11041104

1105-
interceptor = transport_grpc._MTLSCallInterceptor()
1105+
interceptor = transport_grpc.CertRotationInterceptor()
11061106

11071107
def dummy_continuation(*args, **kwargs):
11081108
return mock.Mock()
@@ -1135,7 +1135,7 @@ def test_mtls_refreshing_channel_refresh_logic_cases():
11351135
from unittest import mock
11361136
import google.auth.transport.grpc as transport_grpc
11371137

1138-
channel = transport_grpc._MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
1138+
channel = transport_grpc.MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
11391139
with mock.patch(
11401140
"google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response"
11411141
) as mock_check, mock.patch("google.auth.transport.grpc.secure_authorized_channel"):
@@ -1185,7 +1185,7 @@ def test_mtls_refreshing_channel_subscribe_unsubscribe_close():
11851185
from unittest import mock
11861186
import google.auth.transport.grpc as transport_grpc
11871187

1188-
channel = transport_grpc._MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
1188+
channel = transport_grpc.MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
11891189
cb = mock.Mock()
11901190
channel.subscribe(cb)
11911191
assert cb in channel._subscribers
@@ -1200,14 +1200,14 @@ def test_mtls_refreshing_channel_unary_unary():
12001200
from unittest import mock
12011201
import google.auth.transport.grpc as transport_grpc
12021202

1203-
channel = transport_grpc._MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
1203+
channel = transport_grpc.MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
12041204
res = channel.unary_unary("method")
12051205
assert res is not None
12061206

12071207
from unittest import mock
12081208
import google.auth.transport.grpc as transport_grpc
12091209

1210-
channel = transport_grpc._MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
1210+
channel = transport_grpc.MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
12111211
res = channel.unary_unary("method")
12121212
assert res is not None
12131213

@@ -1216,7 +1216,7 @@ def test_mtls_refreshing_channel_unary_stream():
12161216
from unittest import mock
12171217
import google.auth.transport.grpc as transport_grpc
12181218

1219-
channel = transport_grpc._MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
1219+
channel = transport_grpc.MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
12201220
res = channel.unary_stream("method")
12211221
assert res is not None
12221222

@@ -1225,7 +1225,7 @@ def test_mtls_refreshing_channel_stream_unary():
12251225
from unittest import mock
12261226
import google.auth.transport.grpc as transport_grpc
12271227

1228-
channel = transport_grpc._MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
1228+
channel = transport_grpc.MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
12291229
res = channel.stream_unary("method")
12301230
assert res is not None
12311231

@@ -1234,7 +1234,7 @@ def test_mtls_refreshing_channel_stream_stream():
12341234
from unittest import mock
12351235
import google.auth.transport.grpc as transport_grpc
12361236

1237-
channel = transport_grpc._MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
1237+
channel = transport_grpc.MTLSRefreshingChannel("target", {}, mock.Mock(), b"cert1")
12381238
res = channel.stream_stream("method")
12391239
assert res is not None
12401240

@@ -1248,7 +1248,7 @@ def test_retryable_unary_response_future_methods():
12481248
def dummy_continuation(*args, **kwargs):
12491249
return mock_future
12501250

1251-
interceptor = transport_grpc._MTLSCallInterceptor()
1251+
interceptor = transport_grpc.CertRotationInterceptor()
12521252
interceptor._wrapper = mock.Mock()
12531253
interceptor._wrapper._cached_cert = "cert"
12541254

@@ -1285,7 +1285,7 @@ def test_retryable_stream_response_iterator_methods():
12851285
def dummy_continuation(*args, **kwargs):
12861286
return mock_iterator
12871287

1288-
interceptor = transport_grpc._MTLSCallInterceptor()
1288+
interceptor = transport_grpc.CertRotationInterceptor()
12891289
interceptor._wrapper = mock.Mock()
12901290
interceptor._wrapper._cached_cert = "cert"
12911291

0 commit comments

Comments
 (0)