Skip to content

Commit 35b368a

Browse files
committed
feat: shared network client and token storage
1 parent b436a92 commit 35b368a

6 files changed

Lines changed: 1608 additions & 0 deletions

File tree

‎boxsdk/client/client.py‎

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@
1919
from ..util.datetime_formatter import normalize_date_to_rfc3339_format
2020
from ..util.shared_link import get_shared_link_header
2121
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
2237

2338
if TYPE_CHECKING:
2439
from boxsdk import OAuth2
@@ -2009,3 +2024,318 @@ def get_ai_agent_default_config(
20092024
session=self._session,
20102025
response_object=box_response.json(),
20112026
)
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

Comments
 (0)