|
19 | 19 | from ..util.datetime_formatter import normalize_date_to_rfc3339_format |
20 | 20 | from ..util.shared_link import get_shared_link_header |
21 | 21 | from ..util.deprecation_decorator import deprecated |
| 22 | +from ...auth.developer_token_auth import DeveloperTokenAuth |
| 23 | +from ...auth.jwt_auth import JWTAuth |
| 24 | +from ...auth.ccg_auth import CCGAuth |
| 25 | +from ...auth.oauth2 import OAuth2 as LegacyOAuth2 |
| 26 | +from ...util.token_storage_adapter import LegacyTokenStorageAdapter |
| 27 | + |
| 28 | +from box_sdk_gen.box.developer_token_auth import BoxDeveloperTokenAuth |
| 29 | +from box_sdk_gen.box.oauth import BoxOAuth, OAuthConfig |
| 30 | +from box_sdk_gen.box.jwt_auth import BoxJWTAuth, JWTConfig |
| 31 | +from box_sdk_gen.box.ccg_auth import BoxCCGAuth, CCGConfig |
| 32 | +from box_sdk_gen.client import BoxClient |
| 33 | +from box_sdk_gen.networking.network import NetworkSession |
| 34 | +from box_sdk_gen.networking.base_urls import BaseUrls |
| 35 | +from box_sdk_gen.networking.retries import BoxRetryStrategy |
| 36 | +from box_sdk_gen.schemas.access_token import AccessToken |
22 | 37 |
|
23 | 38 | if TYPE_CHECKING: |
24 | 39 | from boxsdk import OAuth2 |
@@ -2009,3 +2024,318 @@ def get_ai_agent_default_config( |
2009 | 2024 | session=self._session, |
2010 | 2025 | response_object=box_response.json(), |
2011 | 2026 | ) |
| 2027 | + |
| 2028 | + def get_authentication(self, *, token_storage=None): |
| 2029 | + """ |
| 2030 | + Extract authentication configuration from this legacy client and convert it |
| 2031 | + to a generated SDK Authentication object. |
| 2032 | + |
| 2033 | + This method supports the following legacy authentication types: |
| 2034 | + - DeveloperTokenAuth -> BoxDeveloperTokenAuth |
| 2035 | + - OAuth2 -> BoxOAuth |
| 2036 | + - JWTAuth -> BoxJWTAuth |
| 2037 | + - CCGAuth -> BoxCCGAuth |
| 2038 | + |
| 2039 | + :param token_storage: |
| 2040 | + Optional TokenStorage instance for the generated SDK. |
| 2041 | + If not provided, an adapter will be created to bridge legacy token storage. |
| 2042 | + :return: |
| 2043 | + Authentication object compatible with the generated SDK (box_sdk_gen). |
| 2044 | + :raises ValueError: |
| 2045 | + If the authentication type is not supported or required credentials are missing. |
| 2046 | + :raises ValueError: |
| 2047 | + If the authentication type is not supported or required credentials are missing. |
| 2048 | + """ |
| 2049 | + oauth = self._oauth |
| 2050 | + |
| 2051 | + # Developer Token Authentication |
| 2052 | + if isinstance(oauth, DeveloperTokenAuth): |
| 2053 | + token = oauth.access_token |
| 2054 | + if not token: |
| 2055 | + raise ValueError("Developer token is not available") |
| 2056 | + return BoxDeveloperTokenAuth(token=token) |
| 2057 | + |
| 2058 | + # OAuth 2.0 Authentication |
| 2059 | + # Check if it's OAuth2 (but not DeveloperTokenAuth, JWTAuth, or CCGAuth) |
| 2060 | + if isinstance(oauth, LegacyOAuth2) and not isinstance(oauth, (DeveloperTokenAuth, JWTAuth, CCGAuth)): |
| 2061 | + # It's OAuth2 |
| 2062 | + client_id = getattr(oauth, '_client_id', None) |
| 2063 | + client_secret = getattr(oauth, '_client_secret', None) |
| 2064 | + |
| 2065 | + if not client_id or not client_secret: |
| 2066 | + raise ValueError("OAuth2 client_id and client_secret are required") |
| 2067 | + |
| 2068 | + # Create token storage adapter if not provided |
| 2069 | + if token_storage is None: |
| 2070 | + # Create adapter from legacy OAuth2's token storage |
| 2071 | + def get_tokens(): |
| 2072 | + return oauth._get_tokens() |
| 2073 | + |
| 2074 | + def store_tokens(access_token, refresh_token): |
| 2075 | + oauth._store_tokens(access_token, refresh_token) |
| 2076 | + |
| 2077 | + token_storage = LegacyTokenStorageAdapter( |
| 2078 | + get_tokens=get_tokens, |
| 2079 | + store_tokens=store_tokens |
| 2080 | + ) |
| 2081 | + |
| 2082 | + config = OAuthConfig( |
| 2083 | + client_id=client_id, |
| 2084 | + client_secret=client_secret, |
| 2085 | + token_storage=token_storage |
| 2086 | + ) |
| 2087 | + |
| 2088 | + # Pre-populate with existing tokens if available |
| 2089 | + auth = BoxOAuth(config=config) |
| 2090 | + access_token, refresh_token = oauth._get_tokens() |
| 2091 | + if access_token: |
| 2092 | + existing_token = AccessToken( |
| 2093 | + access_token=access_token, |
| 2094 | + refresh_token=refresh_token, |
| 2095 | + expires_in=3600, # Default, actual expiry not available |
| 2096 | + token_type='bearer' |
| 2097 | + ) |
| 2098 | + token_storage.store(existing_token) |
| 2099 | + |
| 2100 | + return auth |
| 2101 | + |
| 2102 | + # JWT Authentication |
| 2103 | + if isinstance(oauth, JWTAuth): |
| 2104 | + client_id = getattr(oauth, '_client_id', None) |
| 2105 | + client_secret = getattr(oauth, '_client_secret', None) |
| 2106 | + jwt_key_id = getattr(oauth, '_jwt_key_id', None) |
| 2107 | + rsa_private_key = getattr(oauth, '_rsa_private_key', None) |
| 2108 | + enterprise_id = getattr(oauth, '_enterprise_id', None) |
| 2109 | + user_id = getattr(oauth, '_user_id', None) |
| 2110 | + |
| 2111 | + if not all([client_id, client_secret, jwt_key_id, rsa_private_key]): |
| 2112 | + raise ValueError("JWT authentication requires client_id, client_secret, jwt_key_id, and private key") |
| 2113 | + |
| 2114 | + # Convert RSA private key to string format |
| 2115 | + # Note: If the key was originally encrypted, we can't extract the passphrase |
| 2116 | + # from the normalized RSAPrivateKey object. We'll serialize it unencrypted. |
| 2117 | + from cryptography.hazmat.primitives import serialization |
| 2118 | + try: |
| 2119 | + # Serialize the key to PEM format (unencrypted) |
| 2120 | + # This works even if the original key was encrypted, as the |
| 2121 | + # normalized RSAPrivateKey object is already decrypted |
| 2122 | + private_key_pem = rsa_private_key.private_bytes( |
| 2123 | + encoding=serialization.Encoding.PEM, |
| 2124 | + format=serialization.PrivateFormat.PKCS8, |
| 2125 | + encryption_algorithm=serialization.NoEncryption() |
| 2126 | + ).decode('utf-8') |
| 2127 | + # Passphrase is not needed since we're serializing unencrypted |
| 2128 | + # The generated SDK will handle encryption if needed |
| 2129 | + passphrase = '' |
| 2130 | + except Exception as e: |
| 2131 | + raise ValueError( |
| 2132 | + f"Cannot serialize private key: {e}. " |
| 2133 | + "Please ensure the private key is valid." |
| 2134 | + ) from e |
| 2135 | + |
| 2136 | + # Create token storage adapter if not provided |
| 2137 | + if token_storage is None: |
| 2138 | + from box_sdk_gen.box.token_storage import InMemoryTokenStorage |
| 2139 | + token_storage = InMemoryTokenStorage() |
| 2140 | + |
| 2141 | + config = JWTConfig( |
| 2142 | + client_id=client_id, |
| 2143 | + client_secret=client_secret, |
| 2144 | + jwt_key_id=jwt_key_id, |
| 2145 | + private_key=private_key_pem, |
| 2146 | + private_key_passphrase=passphrase, |
| 2147 | + enterprise_id=enterprise_id, |
| 2148 | + user_id=user_id, |
| 2149 | + token_storage=token_storage |
| 2150 | + ) |
| 2151 | + |
| 2152 | + auth = BoxJWTAuth(config=config) |
| 2153 | + |
| 2154 | + # Handle user vs enterprise scope |
| 2155 | + if user_id: |
| 2156 | + auth = auth.with_user_subject(user_id, token_storage=token_storage) |
| 2157 | + |
| 2158 | + return auth |
| 2159 | + |
| 2160 | + # CCG (Client Credentials Grant) Authentication |
| 2161 | + if isinstance(oauth, CCGAuth): |
| 2162 | + client_id = getattr(oauth, '_client_id', None) |
| 2163 | + client_secret = getattr(oauth, '_client_secret', None) |
| 2164 | + enterprise_id = getattr(oauth, '_enterprise_id', None) |
| 2165 | + user_id = getattr(oauth, '_user_id', None) |
| 2166 | + |
| 2167 | + if not client_id or not client_secret: |
| 2168 | + raise ValueError("CCG authentication requires client_id and client_secret") |
| 2169 | + |
| 2170 | + # Create token storage adapter if not provided |
| 2171 | + if token_storage is None: |
| 2172 | + from box_sdk_gen.box.token_storage import InMemoryTokenStorage |
| 2173 | + token_storage = InMemoryTokenStorage() |
| 2174 | + |
| 2175 | + config = CCGConfig( |
| 2176 | + client_id=client_id, |
| 2177 | + client_secret=client_secret, |
| 2178 | + enterprise_id=enterprise_id, |
| 2179 | + user_id=user_id, |
| 2180 | + token_storage=token_storage |
| 2181 | + ) |
| 2182 | + |
| 2183 | + auth = BoxCCGAuth(config=config) |
| 2184 | + |
| 2185 | + # Handle user vs enterprise scope |
| 2186 | + if user_id: |
| 2187 | + auth = auth.with_user_subject(user_id, token_storage=token_storage) |
| 2188 | + |
| 2189 | + return auth |
| 2190 | + |
| 2191 | + raise ValueError( |
| 2192 | + f"Unsupported authentication type: {type(oauth).__name__}. " |
| 2193 | + "Supported types: DeveloperTokenAuth, OAuth2, JWTAuth, CCGAuth" |
| 2194 | + ) |
| 2195 | + |
| 2196 | + def get_network_session( |
| 2197 | + self, |
| 2198 | + *, |
| 2199 | + network_client=None, |
| 2200 | + retry_strategy=None, |
| 2201 | + data_sanitizer=None, |
| 2202 | + interceptors=None, |
| 2203 | + additional_headers=None |
| 2204 | + ): |
| 2205 | + """ |
| 2206 | + Extract network configuration from this legacy client and convert it |
| 2207 | + to a generated SDK NetworkSession object. |
| 2208 | + |
| 2209 | + :param network_client: |
| 2210 | + Optional NetworkClient instance for the generated SDK. |
| 2211 | + If not provided, a default will be created with proxy support if configured. |
| 2212 | + :param retry_strategy: |
| 2213 | + Optional RetryStrategy instance for the generated SDK. |
| 2214 | + If not provided, one will be created from legacy retry settings. |
| 2215 | + :param data_sanitizer: |
| 2216 | + Optional DataSanitizer instance for the generated SDK. |
| 2217 | + :param interceptors: |
| 2218 | + Optional list of request/response interceptors. |
| 2219 | + :param additional_headers: |
| 2220 | + Optional dictionary of additional HTTP headers to merge with legacy headers. |
| 2221 | + :return: |
| 2222 | + NetworkSession object compatible with the generated SDK (box_sdk_gen). |
| 2223 | + """ |
| 2224 | + session = self._session |
| 2225 | + api_config = session.api_config |
| 2226 | + proxy_config = session.proxy_config |
| 2227 | + |
| 2228 | + # Extract base URLs |
| 2229 | + base_url = getattr(api_config, 'BASE_API_URL', 'https://api.box.com/2.0') |
| 2230 | + # Remove version suffix if present |
| 2231 | + if base_url.endswith('/2.0'): |
| 2232 | + base_url = base_url[:-4] |
| 2233 | + elif base_url.endswith('/2'): |
| 2234 | + base_url = base_url[:-2] |
| 2235 | + |
| 2236 | + upload_url = getattr(api_config, 'UPLOAD_URL', 'https://upload.box.com/api/2.0') |
| 2237 | + # Remove version suffix if present |
| 2238 | + if upload_url.endswith('/2.0'): |
| 2239 | + upload_url = upload_url[:-4] |
| 2240 | + elif upload_url.endswith('/2'): |
| 2241 | + upload_url = upload_url[:-2] |
| 2242 | + |
| 2243 | + oauth2_url = getattr(api_config, 'OAUTH2_AUTHORIZE_URL', 'https://account.box.com/api/oauth2') |
| 2244 | + # Extract base OAuth URL |
| 2245 | + if '/authorize' in oauth2_url: |
| 2246 | + oauth2_url = oauth2_url[:oauth2_url.rindex('/authorize')] |
| 2247 | + |
| 2248 | + base_urls = BaseUrls( |
| 2249 | + base_url=base_url, |
| 2250 | + upload_url=upload_url, |
| 2251 | + oauth_2_url=oauth2_url |
| 2252 | + ) |
| 2253 | + |
| 2254 | + # Extract or create retry strategy |
| 2255 | + if retry_strategy is None: |
| 2256 | + max_retries = getattr(api_config, 'MAX_RETRY_ATTEMPTS', 5) |
| 2257 | + retry_base_interval = getattr(session, '_retry_base_interval', 1.0) |
| 2258 | + retry_strategy = BoxRetryStrategy( |
| 2259 | + max_attempts=max_retries, |
| 2260 | + retry_base_interval=retry_base_interval |
| 2261 | + ) |
| 2262 | + |
| 2263 | + # Handle proxy configuration |
| 2264 | + proxy_url = None |
| 2265 | + if proxy_config and hasattr(proxy_config, 'URL') and proxy_config.URL: |
| 2266 | + proxy_url = proxy_config.URL |
| 2267 | + # Handle authenticated proxy |
| 2268 | + if hasattr(proxy_config, 'AUTH') and proxy_config.AUTH: |
| 2269 | + auth = proxy_config.AUTH |
| 2270 | + if isinstance(auth, dict) and 'user' in auth and 'password' in auth: |
| 2271 | + # Extract host from URL |
| 2272 | + host = proxy_url.split('//')[1] if '//' in proxy_url else proxy_url |
| 2273 | + proxy_url = f"http://{auth['user']}:{auth['password']}@{host}" |
| 2274 | + |
| 2275 | + # Merge custom headers |
| 2276 | + headers = {} |
| 2277 | + if hasattr(session, '_default_headers'): |
| 2278 | + headers.update(session._default_headers.copy()) |
| 2279 | + if additional_headers: |
| 2280 | + headers.update(additional_headers) |
| 2281 | + |
| 2282 | + # Create network session |
| 2283 | + network_session = NetworkSession( |
| 2284 | + base_urls=base_urls, |
| 2285 | + network_client=network_client, |
| 2286 | + retry_strategy=retry_strategy, |
| 2287 | + additional_headers=headers if headers else None, |
| 2288 | + proxy_url=proxy_url, |
| 2289 | + data_sanitizer=data_sanitizer |
| 2290 | + ) |
| 2291 | + |
| 2292 | + return network_session |
| 2293 | + |
| 2294 | + def get_sdk_gen_client( |
| 2295 | + self, |
| 2296 | + *, |
| 2297 | + auth_options=None, |
| 2298 | + network_options=None |
| 2299 | + ): |
| 2300 | + """ |
| 2301 | + Create a fully configured generated SDK client from this legacy client. |
| 2302 | + |
| 2303 | + This method combines get_authentication() and get_network_session() to create |
| 2304 | + a BoxClient instance that shares authentication and network configuration |
| 2305 | + with this legacy client. |
| 2306 | + |
| 2307 | + :param auth_options: |
| 2308 | + Optional dictionary with authentication options: |
| 2309 | + - token_storage: Custom TokenStorage instance |
| 2310 | + :param network_options: |
| 2311 | + Optional dictionary with network options: |
| 2312 | + - network_client: Custom NetworkClient instance |
| 2313 | + - retry_strategy: Custom RetryStrategy instance |
| 2314 | + - data_sanitizer: Custom DataSanitizer instance |
| 2315 | + - interceptors: List of request/response interceptors |
| 2316 | + - additional_headers: Dictionary of additional HTTP headers |
| 2317 | + :return: |
| 2318 | + BoxClient instance from box_sdk_gen, fully configured with shared settings. |
| 2319 | + """ |
| 2320 | + # Extract authentication |
| 2321 | + token_storage = None |
| 2322 | + if auth_options and 'token_storage' in auth_options: |
| 2323 | + token_storage = auth_options['token_storage'] |
| 2324 | + |
| 2325 | + auth = self.get_authentication(token_storage=token_storage) |
| 2326 | + |
| 2327 | + # Extract network session |
| 2328 | + network_kwargs = {} |
| 2329 | + if network_options: |
| 2330 | + network_kwargs = { |
| 2331 | + 'network_client': network_options.get('network_client'), |
| 2332 | + 'retry_strategy': network_options.get('retry_strategy'), |
| 2333 | + 'data_sanitizer': network_options.get('data_sanitizer'), |
| 2334 | + 'interceptors': network_options.get('interceptors'), |
| 2335 | + 'additional_headers': network_options.get('additional_headers') |
| 2336 | + } |
| 2337 | + |
| 2338 | + network_session = self.get_network_session(**network_kwargs) |
| 2339 | + |
| 2340 | + # Create and return fully configured client |
| 2341 | + return BoxClient(auth=auth, network_session=network_session) |
0 commit comments