44
55import threading
66import time
7+ from abc import ABC , abstractmethod
78from collections import OrderedDict
89from dataclasses import dataclass
910from typing import Any , Dict , Optional , Tuple , Union
1415from .cursor import CommenterCursor , SQLASCIITextLoader
1516
1617
18+ class TokenProvider (ABC ):
19+ """
20+ Interface for providing a token for managed authentication.
21+ """
22+
23+ def __init__ (self , * , skew_seconds : int = 60 ):
24+ self ._skew = skew_seconds
25+ self ._lock = threading .Lock ()
26+ self ._token : str | None = None
27+ self ._expires_at : float = 0.0
28+
29+ def get_token (self ) -> str :
30+ """
31+ Get a token for managed authentication.
32+ """
33+ now = time .time ()
34+ with self ._lock :
35+ if self ._token is None or now >= self ._expires_at - self ._skew :
36+ token , expires_at = self ._fetch_token ()
37+ self ._token = token
38+ self ._expires_at = float (expires_at )
39+ return self ._token # type: ignore[return-value]
40+
41+ @abstractmethod
42+ def _fetch_token (self ) -> Tuple [str , float ]:
43+ """
44+ Return (token, expires_at_epoch_seconds).
45+ Implementations should return the absolute expiry; if the provider
46+ has a fixed TTL, compute expires_at = time.time() + ttl_seconds.
47+ """
48+
49+
50+ class AWSTokenProvider (TokenProvider ):
51+ """
52+ Token provider for AWS IAM authentication.
53+ """
54+
55+ TOKEN_TTL_SECONDS = 900 # 15 minutes
56+
57+ def __init__ (
58+ self , host : str , port : int , username : str , region : str , * , role_arn : str = None , skew_seconds : int = 60
59+ ):
60+ super ().__init__ (skew_seconds = skew_seconds )
61+ self .host = host
62+ self .port = port
63+ self .username = username
64+ self .region = region
65+ self .role_arn = role_arn
66+
67+ def _fetch_token (self ) -> Tuple [str , float ]:
68+ # Import aws only when this method is called
69+ from .aws import generate_rds_iam_token
70+
71+ token = generate_rds_iam_token (
72+ host = self .host , port = self .port , username = self .username , region = self .region , role_arn = self .role_arn
73+ )
74+ return token , time .time () + self .TOKEN_TTL_SECONDS
75+
76+
77+ class AzureTokenProvider (TokenProvider ):
78+ """
79+ Token provider for Azure Managed Identity.
80+ """
81+
82+ def __init__ (self , client_id : str , identity_scope : str = None , skew_seconds : int = 60 ):
83+ super ().__init__ (skew_seconds = skew_seconds )
84+ self .client_id = client_id
85+ self .identity_scope = identity_scope
86+
87+ def _fetch_token (self ) -> Tuple [str , float ]:
88+ # Import azure only when this method is called
89+ from .azure import generate_managed_identity_token
90+
91+ token = generate_managed_identity_token (client_id = self .client_id , identity_scope = self .identity_scope )
92+ return token .token , float (token .expires_at )
93+
94+
95+ class TokenAwareConnection (Connection ):
96+ """
97+ Connection that can be used for managed authentication.
98+ """
99+
100+ token_provider : Optional [TokenProvider ] = None
101+
102+ @classmethod
103+ def connect (cls , * args , ** kwargs ):
104+ """
105+ Override the connection method to pass a refreshable token as the connection password.
106+ """
107+ if cls .token_provider :
108+ kwargs ["password" ] = cls .token_provider .get_token ()
109+ return super ().connect (* args , ** kwargs )
110+
111+
17112@dataclass (frozen = True )
18113class PostgresConnectionArgs :
19114 """
@@ -25,6 +120,7 @@ class PostgresConnectionArgs:
25120 host : Optional [str ] = None
26121 port : Optional [int ] = None
27122 password : Optional [str ] = None
123+ token_provider : Optional [TokenProvider ] = None
28124 ssl_mode : Optional [str ] = "allow"
29125 ssl_cert : Optional [str ] = None
30126 ssl_root_cert : Optional [str ] = None
@@ -86,6 +182,7 @@ def __init__(
86182 pool_config : Optional [Dict [str , Any ]] = None ,
87183 statement_timeout : Optional [int ] = None , # milliseconds
88184 sqlascii_encodings : Optional [list [str ]] = None ,
185+ token_provider : Optional [TokenProvider ] = None ,
89186 ) -> None :
90187 """
91188 Initialize the pool manager.
@@ -101,13 +198,17 @@ def __init__(
101198 self .base_conn_args = base_conn_args
102199 self .statement_timeout = statement_timeout
103200 self .sqlascii_encodings = sqlascii_encodings
201+ self .token_provider = token_provider
104202
105203 self .pool_config = {
106204 ** (pool_config or {}),
107205 "min_size" : 0 ,
108206 "max_size" : 2 ,
109207 "open" : True ,
110208 }
209+
210+ TokenAwareConnection .token_provider = self .token_provider
211+
111212 self .lock = threading .Lock ()
112213 self .pools : OrderedDict [str , Tuple [ConnectionPool , float , bool ]] = OrderedDict ()
113214 self ._closed = False
@@ -139,7 +240,12 @@ def _create_pool(self, dbname: str) -> ConnectionPool:
139240 """
140241 kwargs = self .base_conn_args .as_kwargs (dbname = dbname )
141242
142- return ConnectionPool (kwargs = kwargs , configure = self ._configure_connection , ** self .pool_config )
243+ return ConnectionPool (
244+ kwargs = kwargs ,
245+ configure = self ._configure_connection ,
246+ connection_class = TokenAwareConnection ,
247+ ** self .pool_config ,
248+ )
143249
144250 def get_connection (self , dbname : str , persistent : bool = False ):
145251 """
0 commit comments