Skip to content

Commit 7b3f17b

Browse files
fix: Fix lint and unit tests
Signed-off-by: Radhika Agrawal <agrawalradhika@google.com>
1 parent 91570a3 commit 7b3f17b

2 files changed

Lines changed: 192 additions & 126 deletions

File tree

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

Lines changed: 98 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525

2626
from google.auth import exceptions
27-
from google.auth import transport
27+
from google.auth import transport
2828
from google.auth.transport import _mtls_helper
2929
from google.auth.transport import mtls
3030
from google.oauth2 import service_account
@@ -134,6 +134,8 @@ def secure_authorized_channel(
134134
target,
135135
ssl_credentials=None,
136136
client_cert_callback=None,
137+
*,
138+
auto_refresh: bool = True,
137139
**kwargs
138140
):
139141
"""Creates a secure authorized gRPC channel.
@@ -288,7 +290,8 @@ def my_client_cert_callback():
288290
)
289291

290292
# If SSL credentials are not explicitly set, try client_cert_callback and ADC.
291-
cached_cert = None
293+
import typing
294+
cached_cert: typing.Optional[bytes] = None
292295
if not ssl_credentials:
293296
use_client_cert = _mtls_helper.check_use_client_cert()
294297
if use_client_cert and client_cert_callback:
@@ -310,25 +313,21 @@ def my_client_cert_callback():
310313
composite_credentials = grpc.composite_channel_credentials(
311314
ssl_credentials, google_auth_credentials
312315
)
313-
is_recreation = kwargs.pop("_is_recreation", False)
314316
channel = grpc.secure_channel(target, composite_credentials, **kwargs)
315-
# Avoid wrapping if mTLS is disabled or if this is a channel recreation call
316-
if cached_cert and not is_recreation:
317-
# Package arguments so the channel can be recreated later
318-
factory_args = {
319-
"credentials": credentials,
320-
"request": request,
321-
"target": target,
322-
"ssl_credentials": None,
323-
"client_cert_callback": client_cert_callback,
324-
"_is_recreation": True, # Hidden flag to stop recursion
317+
# Avoid wrapping if mTLS is disabled or if auto_refresh is False
318+
if cached_cert and auto_refresh:
319+
import functools
320+
channel_fn = functools.partial(
321+
secure_authorized_channel,
322+
credentials,
323+
request,
324+
target,
325+
client_cert_callback=None,
326+
auto_refresh=False,
325327
**kwargs,
326-
}
327-
interceptor = _MTLSCallInterceptor()
328-
329-
wrapper = _MTLSRefreshingChannel(target, factory_args, channel, cached_cert)
330-
331-
interceptor._wrapper = wrapper
328+
)
329+
wrapper = MTLSRefreshingChannel(target, channel_fn, channel, cached_cert)
330+
interceptor = CertRotationInterceptor(wrapper=wrapper)
332331
return grpc.intercept_channel(wrapper, interceptor)
333332
return channel
334333

@@ -354,7 +353,8 @@ class SslCredentials:
354353

355354
def __init__(self):
356355
use_client_cert = _mtls_helper.check_use_client_cert()
357-
self._cached_cert = None
356+
self._import typing
357+
cached_cert: typing.Optional[bytes] = None
358358
if not use_client_cert:
359359
self._is_mtls = False
360360
else:
@@ -401,37 +401,39 @@ 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,
408408
grpc.StreamStreamClientInterceptor,
409409
):
410410
"""A gRPC client interceptor that provides automatic retry logic for mTLS certificate rotation.
411-
412-
This interceptor wraps all gRPC client calls (unary and streaming) with retryable
413-
futures or iterators. Its primary role is to monitor responses for `UNAUTHENTICATED`
414-
errors. When an authentication failure occurs, it uses `_should_retry()` to check
415-
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
411+
412+
This interceptor wraps all gRPC client calls (unary and streaming) with retryable
413+
futures or iterators. Its primary role is to monitor responses for `UNAUTHENTICATED`
414+
errors. When an authentication failure occurs, it uses `_should_retry()` to check
415+
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
417417
channel's credentials and automatically replays the failed RPC.
418418
"""
419419

420-
def __init__(self):
421-
self._wrapper = None
420+
def __init__(self, wrapper=None):
421+
self._wrapper = wrapper
422422
self._max_retries = transport.DEFAULT_MAX_REFRESH_ATTEMPTS
423423

424424
def _should_retry(self, code, retry_count, attempt_cert):
425425
"""Determines if the RPC should be retried due to a certificate rotation.
426-
427-
Returns a tuple: (should_retry, call_cert_bytes, call_key_bytes, passphrase).
428-
"""
426+
427+
Returns a tuple: (should_retry, call_cert_bytes, call_key_bytes, passphrase).
428+
"""
429429
if code != grpc.StatusCode.UNAUTHENTICATED or not self._wrapper:
430430
return False, None, None, None
431431

432432
if retry_count >= self._max_retries:
433433
_LOGGER.debug(
434-
"Max retries reached (%d/%d) for channel recreation.", retry_count, self._max_retries
434+
"Max retries reached (%d/%d) for channel recreation.",
435+
retry_count,
436+
self._max_retries,
435437
)
436438
return False, None, None, None
437439

@@ -483,39 +485,37 @@ def intercept_stream_stream(
483485
)
484486

485487

486-
class _MTLSRefreshingChannel(grpc.Channel):
487-
def __init__(self, target, factory_args, initial_channel, initial_cert):
488+
class MTLSRefreshingChannel(grpc.Channel):
489+
def __init__(self, target, channel_fn, initial_channel, initial_cert):
488490
self._target = target
489-
self._factory_args = factory_args
491+
self._channel_fn = channel_fn
490492
self._channel = initial_channel
491493
self._cached_cert = initial_cert
492494
self._lock = threading.Lock()
493495
self._subscribers = set()
494496

495-
def refresh_logic(self, count, call_cert_bytes=None, call_key_bytes=None, passphrase=None):
497+
def refresh_logic(
498+
self, count, call_cert_bytes=None, call_key_bytes=None, passphrase=None
499+
):
496500
with self._lock:
497501
if not call_cert_bytes or self._cached_cert == call_cert_bytes:
498502
return
499503

500-
_LOGGER.debug(
501-
"Wrapper: Refreshing mTLS channel. Retry count: %d", count
502-
)
504+
_LOGGER.debug("Wrapper: Refreshing mTLS channel. Retry count: %d", count)
503505
old_channel = self._channel
504506

505507
if passphrase is not None:
506508
call_key_bytes = _mtls_helper.decrypt_private_key(
507509
call_key_bytes, passphrase
508510
)
509511

510-
# The factory args must use the new credentials exactly to build the rotation channel
511-
factory_args = self._factory_args.copy()
512-
factory_args["client_cert_callback"] = None
513-
factory_args["ssl_credentials"] = grpc.ssl_channel_credentials(
512+
new_ssl_credentials = grpc.ssl_channel_credentials(
514513
certificate_chain=call_cert_bytes,
515514
private_key=call_key_bytes,
516515
)
517-
518-
self._channel = secure_authorized_channel(**factory_args)
516+
self._channel = self._channel_fn(
517+
ssl_credentials=new_ssl_credentials
518+
)
519519
self._cached_cert = call_cert_bytes
520520
for callback in self._subscribers:
521521
try:
@@ -630,15 +630,19 @@ def __next__(self):
630630
("method", "timeout", "metadata", "credentials", "wait_for_ready"),
631631
)
632632

633+
633634
class _DeadlineExceededError(grpc.RpcError, grpc.Call):
634635
def __init__(self, details):
635636
super().__init__()
636637
self._details = details
638+
637639
def code(self):
638640
return grpc.StatusCode.DEADLINE_EXCEEDED
641+
639642
def details(self):
640643
return self._details
641644

645+
642646
class _RetryableUnaryResponseFuture(grpc.Future, grpc.Call):
643647
def __init__(
644648
self,
@@ -698,7 +702,9 @@ def _start_call(self):
698702
elapsed = time.monotonic() - self._start_time
699703
remaining = self._initial_timeout - elapsed
700704
if remaining <= 0:
701-
raise _DeadlineExceededError("Deadline Exceeded during retry resolution.")
705+
raise _DeadlineExceededError(
706+
"Deadline Exceeded during retry resolution."
707+
)
702708
call_details = _ClientCallDetails(
703709
method=call_details.method,
704710
timeout=remaining,
@@ -742,7 +748,9 @@ def _on_inner_future_done(self, inner_future):
742748
if can_replay and should_retry:
743749
if getattr(self._interceptor, "_wrapper", None):
744750
try:
745-
self._interceptor._wrapper.refresh_logic(1, call_cert, call_key, pwd)
751+
self._interceptor._wrapper.refresh_logic(
752+
1, call_cert, call_key, pwd
753+
)
746754
except Exception as e:
747755
with self._lock:
748756
self._terminal_exception = e
@@ -757,18 +765,23 @@ def _on_inner_future_done(self, inner_future):
757765
self._terminal_exception = e
758766

759767
if self._interceptor._wrapper:
760-
chk_should_retry, chk_cert, chk_key, chk_pwd = self._interceptor._should_retry(
761-
status_code, 0, self._attempt_cert
762-
)
768+
(
769+
chk_should_retry,
770+
chk_cert,
771+
chk_key,
772+
chk_pwd,
773+
) = self._interceptor._should_retry(status_code, 0, self._attempt_cert)
763774
if chk_should_retry:
764775
try:
765-
self._interceptor._wrapper.refresh_logic(1, chk_cert, chk_key, chk_pwd)
776+
self._interceptor._wrapper.refresh_logic(
777+
1, chk_cert, chk_key, chk_pwd
778+
)
766779
except Exception:
767780
pass
768781
with self._lock:
769782
self._completion_event.set()
770783
callbacks_to_fire = list(self._done_callbacks)
771-
784+
772785
for fn in callbacks_to_fire:
773786
try:
774787
fn(self)
@@ -929,7 +942,9 @@ def _start_call(self):
929942
elapsed = time.monotonic() - self._start_time
930943
remaining = self._initial_timeout - elapsed
931944
if remaining <= 0:
932-
raise _DeadlineExceededError("Deadline Exceeded during retry resolution.")
945+
raise _DeadlineExceededError(
946+
"Deadline Exceeded during retry resolution."
947+
)
933948
call_details = _ClientCallDetails(
934949
method=call_details.method,
935950
timeout=remaining,
@@ -959,9 +974,12 @@ def _on_inner_call_done(self, inner_call):
959974
if self._call is not inner_call:
960975
return
961976
# Intercept and suppress premature callbacks for UNAUTHENTICATED.
962-
# __next__ inherently handles this error and manages triggering callbacks
977+
# __next__ inherently handles this error and manages triggering callbacks
963978
# later if retriies are exhausted.
964-
if callable(getattr(inner_call, "code", None)) and inner_call.code() == grpc.StatusCode.UNAUTHENTICATED:
979+
if (
980+
callable(getattr(inner_call, "code", None))
981+
and inner_call.code() == grpc.StatusCode.UNAUTHENTICATED
982+
):
965983
return
966984
self._trigger_callbacks()
967985

@@ -994,42 +1012,52 @@ def __next__(self):
9941012
)
9951013
)
9961014

997-
should_retry, call_cert, call_key, pwd = self._interceptor._should_retry(
1015+
(
1016+
should_retry,
1017+
call_cert,
1018+
call_key,
1019+
pwd,
1020+
) = self._interceptor._should_retry(
9981021
status_code,
9991022
self._retry_count,
10001023
getattr(self, "_attempt_cert", None),
10011024
)
10021025

1003-
if (
1004-
not self._yielded_any_response
1005-
and can_replay
1006-
and should_retry
1007-
):
1026+
if not self._yielded_any_response and can_replay and should_retry:
10081027
try:
10091028
if getattr(self._interceptor, "_wrapper", None):
1010-
self._interceptor._wrapper.refresh_logic(1, call_cert, call_key, pwd)
1011-
1029+
self._interceptor._wrapper.refresh_logic(
1030+
1, call_cert, call_key, pwd
1031+
)
1032+
10121033
with self._lock:
10131034
self._retry_count += 1
10141035
self._start_call()
1015-
1036+
10161037
except Exception as fallback_e:
10171038
self._trigger_callbacks()
10181039
raise fallback_e
1019-
1040+
10201041
continue
10211042
else:
10221043
# Non-retryable error, check if another rotation happened while we were finishing
10231044
if getattr(self._interceptor, "_wrapper", None):
1024-
chk_should_retry, chk_cert, chk_key, chk_pwd = self._interceptor._should_retry(
1045+
(
1046+
chk_should_retry,
1047+
chk_cert,
1048+
chk_key,
1049+
chk_pwd,
1050+
) = self._interceptor._should_retry(
10251051
status_code, 0, getattr(self, "_attempt_cert", None)
10261052
)
10271053
if chk_should_retry:
10281054
try:
1029-
self._interceptor._wrapper.refresh_logic(1, chk_cert, chk_key, chk_pwd)
1055+
self._interceptor._wrapper.refresh_logic(
1056+
1, chk_cert, chk_key, chk_pwd
1057+
)
10301058
except Exception:
1031-
pass # Terminal anyway
1032-
1059+
pass # Terminal anyway
1060+
10331061
self._trigger_callbacks()
10341062
raise e
10351063

0 commit comments

Comments
 (0)