2626from google .cloud .sql .connector .connection_info import ConnectionInfo
2727from google .cloud .sql .connector .connection_info import ConnectionInfoCache
2828from google .cloud .sql .connector .connection_name import ConnectionName
29+ from google .cloud .sql .connector .enums import IPTypes
2930from google .cloud .sql .connector .exceptions import RefreshNotValidError
3031from google .cloud .sql .connector .rate_limiter import AsyncRateLimiter
3132from google .cloud .sql .connector .refresh_utils import _is_valid
3435logger = logging .getLogger (name = __name__ )
3536
3637APPLICATION_NAME = "cloud-sql-python-connector"
38+ SERVER_PROXY_PORT = 3307
39+ DEFAULT_CONNECT_TIMEOUT = 30
3740
3841
3942class RefreshAheadCache (ConnectionInfoCache ):
@@ -50,6 +53,8 @@ def __init__(
5053 client : CloudSQLClient ,
5154 keys : asyncio .Future ,
5255 enable_iam_auth : bool = False ,
56+ ip_type : IPTypes | str = IPTypes .PUBLIC ,
57+ timeout : int = DEFAULT_CONNECT_TIMEOUT ,
5358 ) -> None :
5459 """Initializes a RefreshAheadCache instance.
5560
@@ -62,21 +67,32 @@ def __init__(
6267 enable_iam_auth (bool): Enables automatic IAM database authentication
6368 (Postgres and MySQL) as the default authentication method for all
6469 connections.
70+ ip_type (IPTypes | str): Preferred IP type used to connect to the instance.
71+ timeout (int): Connect timeout in seconds.
6572 """
6673 self ._conn_name = conn_name
6774
6875 self ._enable_iam_auth = enable_iam_auth
76+ if isinstance (ip_type , str ):
77+ ip_type = IPTypes ._from_str (ip_type )
78+ self ._ip_type = ip_type
79+ self ._timeout = timeout
6980 self ._keys = keys
7081 self ._client = client
7182 self ._refresh_rate_limiter = AsyncRateLimiter (
7283 max_capacity = 2 ,
7384 rate = 1 / 30 ,
7485 )
7586 self ._refresh_in_progress = asyncio .locks .Event ()
87+ self ._iam_principals : list [tuple [str , str ]] = []
7688 self ._current : asyncio .Task = self ._schedule_refresh (0 )
7789 self ._next : asyncio .Task = self ._current
7890 self ._closed = False
7991
92+ def record_principal (self , user : str , database : str ) -> None :
93+ """Records a PostgreSQL (user, database) pair observed on an IAM connection."""
94+ _append_iam_principal (self ._iam_principals , user , database )
95+
8096 @property
8197 def conn_name (self ) -> ConnectionName :
8298 return self ._conn_name
@@ -119,6 +135,8 @@ async def _perform_refresh(self) -> ConnectionInfo:
119135 self ._keys ,
120136 self ._enable_iam_auth ,
121137 )
138+ if self ._enable_iam_auth :
139+ await self ._probe_connection (connection_info )
122140 logger .debug (
123141 f"['{ self ._conn_name } ']: Connection info refresh operation complete"
124142 )
@@ -138,6 +156,17 @@ async def _perform_refresh(self) -> ConnectionInfo:
138156 self ._refresh_in_progress .clear ()
139157 return connection_info
140158
159+ async def _probe_connection (self , conn_info : ConnectionInfo ) -> None :
160+ """Proactively probes the database to refresh IAM tokens on server-side MCP."""
161+ await _probe_instance_connection (
162+ self ._conn_name ,
163+ conn_info ,
164+ self ._enable_iam_auth ,
165+ self ._ip_type ,
166+ self ._timeout ,
167+ self ._iam_principals ,
168+ )
169+
141170 def _schedule_refresh (self , delay : int ) -> asyncio .Task :
142171 """
143172 Schedule task to sleep and then perform refresh to get ConnectionInfo.
@@ -223,3 +252,124 @@ async def close(self) -> None:
223252 tasks = asyncio .gather (self ._current , self ._next , return_exceptions = True )
224253 await asyncio .wait_for (tasks , timeout = 2.0 )
225254 self ._closed = True
255+
256+
257+ def _append_iam_principal (
258+ principals : list [tuple [str , str ]], user : str , database : str
259+ ) -> None :
260+ """Appends (user, database) to principals if not already present."""
261+ if not user :
262+ return
263+ if not database :
264+ database = user
265+ pair = (user , database )
266+ if pair not in principals :
267+ principals .append (pair )
268+
269+
270+ async def _probe_instance_connection (
271+ conn_name : ConnectionName ,
272+ conn_info : ConnectionInfo ,
273+ enable_iam_auth : bool ,
274+ ip_type : IPTypes ,
275+ timeout : int ,
276+ iam_principals : list [tuple [str , str ]],
277+ ) -> None :
278+ """Proactively probes the database to refresh IAM tokens on server-side MCP."""
279+ targets : list [str ] = []
280+ if conn_name .domain_name :
281+ targets .append (conn_name .domain_name )
282+ else :
283+ if ip_type .value in conn_info .ip_addrs :
284+ targets .extend (conn_info .ip_addrs [ip_type .value ])
285+
286+ if not targets :
287+ logger .debug (
288+ f"['{ conn_name } ']: Proactive IAM token refresh probe skipped: no target IP addresses"
289+ )
290+ return
291+
292+ port = SERVER_PROXY_PORT
293+ try :
294+ ssl_context = await conn_info .create_ssl_context (enable_iam_auth )
295+ except Exception as e : # noqa: BLE001
296+ logger .debug (
297+ f"['{ conn_name } ']: Failed to create SSL context for probe: { e !s} "
298+ )
299+ return
300+
301+ principals : list [tuple [str , str ] | None ] = []
302+ if conn_info .database_version .startswith ("POSTGRES" ) and iam_principals :
303+ principals .extend (iam_principals )
304+ else :
305+ principals .append (None )
306+
307+ all_succeeded = True
308+ for principal in principals :
309+ probed_principal = False
310+ for target in targets :
311+ try :
312+ logger .debug (
313+ f"['{ conn_name } ']: Probing IAM token refresh on { target } :{ port } "
314+ )
315+ reader , writer = await asyncio .wait_for (
316+ asyncio .open_connection (
317+ host = target ,
318+ port = port ,
319+ ssl = ssl_context ,
320+ server_hostname = (
321+ conn_name .domain_name
322+ if conn_name .domain_name
323+ else None
324+ ),
325+ ),
326+ timeout = float (timeout ),
327+ )
328+ try :
329+ if principal is not None :
330+ user , database = principal
331+ writer .write (
332+ _build_postgres_startup_packet (user , database )
333+ )
334+ await writer .drain ()
335+ await asyncio .wait_for (
336+ reader .read (1024 ),
337+ timeout = float (timeout ),
338+ )
339+ writer .write (b"X\x00 \x00 \x00 \x04 " )
340+ await writer .drain ()
341+ finally :
342+ writer .close ()
343+ await writer .wait_closed ()
344+ probed_principal = True
345+ break
346+ except Exception as e : # noqa: BLE001
347+ logger .debug (
348+ f"['{ conn_name } ']: Probing IAM token refresh on { target } :{ port } failed: { e !s} "
349+ )
350+ if not probed_principal :
351+ all_succeeded = False
352+
353+ if all_succeeded :
354+ logger .debug (
355+ f"['{ conn_name } ']: Proactive IAM token refresh probe successful"
356+ )
357+ else :
358+ logger .debug (
359+ f"['{ conn_name } ']: Proactive IAM token refresh probe encountered error across all targets"
360+ )
361+
362+
363+ def _build_postgres_startup_packet (user : str , database : str ) -> bytes :
364+ """Builds a PostgreSQL v3.0 StartupMessage for (user, database)."""
365+ if not database :
366+ database = user
367+ body = (
368+ b"user\x00 "
369+ + user .encode ("utf-8" )
370+ + b"\x00 database\x00 "
371+ + database .encode ("utf-8" )
372+ + b"\x00 \x00 "
373+ )
374+ total_len = 8 + len (body )
375+ return total_len .to_bytes (4 , byteorder = "big" ) + b"\x00 \x03 \x00 \x00 " + body
0 commit comments