2424
2525
2626from google .auth import exceptions
27- from google .auth import transport
27+ from google .auth import transport
2828from google .auth .transport import _mtls_helper
2929from google .auth .transport import mtls
3030from 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+
633634class _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+
642646class _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