cli_profile.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. import os
  2. import json
  3. import threading
  4. import platform
  5. from typing import Any, Dict
  6. import aiofiles
  7. # 跨平台文件锁支持
  8. if platform.system() == 'Windows':
  9. # Windows平台使用msvcrt
  10. import msvcrt
  11. HAS_MSVCRT = True
  12. HAS_FCNTL = False
  13. else:
  14. # 其他平台尝试使用fcntl,如果不可用则不设文件锁
  15. HAS_MSVCRT = False
  16. try:
  17. import fcntl
  18. HAS_FCNTL = True
  19. except ImportError:
  20. HAS_FCNTL = False
  21. from .static_ak import StaticAKCredentialsProvider
  22. from .ecs_ram_role import EcsRamRoleCredentialsProvider
  23. from .ram_role_arn import RamRoleArnCredentialsProvider
  24. from .oidc import OIDCRoleArnCredentialsProvider
  25. from .static_sts import StaticSTSCredentialsProvider
  26. from .cloud_sso import CloudSSOCredentialsProvider
  27. from .oauth import OAuthCredentialsProvider, OAuthTokenUpdateCallback, OAuthTokenUpdateCallbackAsync
  28. from .external import (
  29. ExternalCredentialsProvider,
  30. ExternalCredentialUpdateCallback,
  31. ExternalCredentialUpdateCallbackAsync,
  32. )
  33. from .refreshable import Credentials
  34. from alibabacloud_credentials_api import ICredentialsProvider
  35. from alibabacloud_credentials.utils import auth_constant as ac
  36. from alibabacloud_credentials.utils import auth_util as au
  37. from alibabacloud_credentials.exceptions import CredentialException
  38. async def _load_config_async(file_path: str) -> Any:
  39. async with aiofiles.open(file_path, mode='r') as f:
  40. content = await f.read()
  41. return json.loads(content)
  42. def _load_config(file_path: str) -> Any:
  43. with open(file_path, mode='r') as f:
  44. content = f.read()
  45. return json.loads(content)
  46. class CLIProfileCredentialsProvider(ICredentialsProvider):
  47. def __init__(self, *,
  48. profile_name: str = None,
  49. profile_file: str = None,
  50. allow_config_force_rewrite: bool = False):
  51. self._profile_file = profile_file or os.path.join(ac.HOME, ".aliyun", "config.json")
  52. self._profile_name = profile_name or au.environment_profile_name
  53. self._allow_config_force_rewrite = allow_config_force_rewrite
  54. self.__innerProvider = None
  55. # 文件锁,用于并发安全
  56. self._file_lock = threading.RLock()
  57. def _should_reload_credentials_provider(self) -> bool:
  58. if self.__innerProvider is None:
  59. return True
  60. return False
  61. def get_credentials(self) -> Credentials:
  62. if au.environment_cli_profile_disabled.lower() == "true":
  63. raise CredentialException('cli credentials file is disabled')
  64. if self._should_reload_credentials_provider():
  65. if not os.path.exists(self._profile_file) or not os.path.isfile(self._profile_file):
  66. raise CredentialException(f'unable to open credentials file: {self._profile_file}')
  67. try:
  68. config = _load_config(self._profile_file)
  69. except Exception as e:
  70. raise CredentialException(
  71. f'failed to parse credential form cli credentials file: {self._profile_file}')
  72. if config is None:
  73. raise CredentialException(
  74. f'failed to parse credential form cli credentials file: {self._profile_file}')
  75. profile_name = self._profile_name
  76. if self._profile_name is None or self._profile_name == '':
  77. profile_name = config.get('current')
  78. self.__innerProvider = self._get_credentials_provider(config, profile_name)
  79. cre = self.__innerProvider.get_credentials()
  80. credentials = Credentials(
  81. access_key_id=cre.get_access_key_id(),
  82. access_key_secret=cre.get_access_key_secret(),
  83. security_token=cre.get_security_token(),
  84. provider_name=f'{self.get_provider_name()}/{cre.get_provider_name()}'
  85. )
  86. return credentials
  87. async def get_credentials_async(self) -> Credentials:
  88. if au.environment_cli_profile_disabled.lower() == "true":
  89. raise CredentialException('cli credentials file is disabled')
  90. if self._should_reload_credentials_provider():
  91. if not os.path.exists(self._profile_file) or not os.path.isfile(self._profile_file):
  92. raise CredentialException(f'unable to open credentials file: {self._profile_file}')
  93. try:
  94. config = await _load_config_async(self._profile_file)
  95. except Exception as e:
  96. raise CredentialException(
  97. f'failed to parse credential form cli credentials file: {self._profile_file}')
  98. if config is None:
  99. raise CredentialException(
  100. f'failed to parse credential form cli credentials file: {self._profile_file}')
  101. profile_name = self._profile_name
  102. if self._profile_name is None or self._profile_name == '':
  103. profile_name = config.get('current')
  104. self.__innerProvider = self._get_credentials_provider(config, profile_name)
  105. cre = await self.__innerProvider.get_credentials_async()
  106. credentials = Credentials(
  107. access_key_id=cre.get_access_key_id(),
  108. access_key_secret=cre.get_access_key_secret(),
  109. security_token=cre.get_security_token(),
  110. provider_name=f'{self.get_provider_name()}/{cre.get_provider_name()}'
  111. )
  112. return credentials
  113. def _get_credentials_provider(self, config: Dict, profile_name: str) -> ICredentialsProvider:
  114. if profile_name is None or profile_name == '':
  115. raise CredentialException('invalid profile name')
  116. profiles = config.get('profiles', [])
  117. if not profiles:
  118. raise CredentialException(f"unable to get profile with '{profile_name}' form cli credentials file.")
  119. for profile in profiles:
  120. if profile.get('name') is not None and profile['name'] == profile_name:
  121. mode = profile.get('mode')
  122. if mode == "AK":
  123. return StaticAKCredentialsProvider(
  124. access_key_id=profile.get('access_key_id'),
  125. access_key_secret=profile.get('access_key_secret')
  126. )
  127. elif mode == "StsToken":
  128. return StaticSTSCredentialsProvider(
  129. access_key_id=profile.get('access_key_id'),
  130. access_key_secret=profile.get('access_key_secret'),
  131. security_token=profile.get('sts_token')
  132. )
  133. elif mode == "RamRoleArn":
  134. pre_provider = StaticAKCredentialsProvider(
  135. access_key_id=profile.get('access_key_id'),
  136. access_key_secret=profile.get('access_key_secret')
  137. )
  138. return RamRoleArnCredentialsProvider(
  139. credentials_provider=pre_provider,
  140. role_arn=profile.get('ram_role_arn'),
  141. role_session_name=profile.get('ram_session_name'),
  142. duration_seconds=profile.get('expired_seconds'),
  143. policy=profile.get('policy'),
  144. external_id=profile.get('external_id'),
  145. sts_region_id=profile.get('sts_region'),
  146. enable_vpc=profile.get('enable_vpc'),
  147. )
  148. elif mode == "EcsRamRole":
  149. return EcsRamRoleCredentialsProvider(
  150. role_name=profile.get('ram_role_name')
  151. )
  152. elif mode == "OIDC":
  153. return OIDCRoleArnCredentialsProvider(
  154. role_arn=profile.get('ram_role_arn'),
  155. oidc_provider_arn=profile.get('oidc_provider_arn'),
  156. oidc_token_file_path=profile.get('oidc_token_file'),
  157. role_session_name=profile.get('role_session_name'),
  158. duration_seconds=profile.get('expired_seconds'),
  159. policy=profile.get('policy'),
  160. sts_region_id=profile.get('sts_region'),
  161. enable_vpc=profile.get('enable_vpc'),
  162. )
  163. elif mode == "ChainableRamRoleArn":
  164. previous_provider = self._get_credentials_provider(config, profile.get('source_profile'))
  165. return RamRoleArnCredentialsProvider(
  166. credentials_provider=previous_provider,
  167. role_arn=profile.get('ram_role_arn'),
  168. role_session_name=profile.get('ram_session_name'),
  169. duration_seconds=profile.get('expired_seconds'),
  170. policy=profile.get('policy'),
  171. external_id=profile.get('external_id'),
  172. sts_region_id=profile.get('sts_region'),
  173. enable_vpc=profile.get('enable_vpc'),
  174. )
  175. elif mode == "CloudSSO":
  176. return CloudSSOCredentialsProvider(
  177. sign_in_url=profile.get('cloud_sso_sign_in_url'),
  178. account_id=profile.get('cloud_sso_account_id'),
  179. access_config=profile.get('cloud_sso_access_config'),
  180. access_token=profile.get('access_token'),
  181. access_token_expire=profile.get('cloud_sso_access_token_expire'),
  182. )
  183. elif mode == "OAuth":
  184. # 获取 OAuth 配置
  185. site_type = profile.get('oauth_site_type', 'CN')
  186. oauth_base_url_map = {
  187. 'CN': 'https://oauth.aliyun.com',
  188. 'INTL': 'https://oauth.alibabacloud.com'
  189. }
  190. sign_in_url = oauth_base_url_map.get(site_type.upper())
  191. if not sign_in_url:
  192. raise CredentialException('Invalid OAuth site type, support CN or INTL')
  193. oauth_client_map = {
  194. 'CN': '4038181954557748008',
  195. 'INTL': '4103531455503354461'
  196. }
  197. client_id = oauth_client_map.get(site_type.upper())
  198. if not client_id:
  199. raise CredentialException('Invalid OAuth site type, support CN or INTL')
  200. return OAuthCredentialsProvider(
  201. client_id=client_id,
  202. sign_in_url=sign_in_url,
  203. access_token=profile.get('oauth_access_token'),
  204. access_token_expire=profile.get('oauth_access_token_expire'),
  205. refresh_token=profile.get('oauth_refresh_token'),
  206. # Reuse the STS credential persisted by a previous
  207. # exchange until it expires, instead of exchanging on
  208. # every resolve (matches aliyun-cli behaviour).
  209. access_key_id=profile.get('access_key_id'),
  210. access_key_secret=profile.get('access_key_secret'),
  211. security_token=profile.get('sts_token'),
  212. sts_expiration=profile.get('sts_expiration'),
  213. token_update_callback=self._get_oauth_token_update_callback(),
  214. token_update_callback_async=self._get_oauth_token_update_callback_async(),
  215. )
  216. elif mode == "External":
  217. return ExternalCredentialsProvider(
  218. process_command=profile.get('process_command'),
  219. credential_update_callback=self._get_external_credential_update_callback(),
  220. credential_update_callback_async=self._get_external_credential_update_callback_async(),
  221. )
  222. else:
  223. raise CredentialException(f"unsupported profile mode '{mode}' form cli credentials file.")
  224. raise CredentialException(f"unable to get profile with '{profile_name}' form cli credentials file.")
  225. def get_provider_name(self) -> str:
  226. return 'cli_profile'
  227. def _update_oauth_tokens(self, refresh_token: str, access_token: str, access_key: str, secret: str,
  228. security_token: str, access_token_expire: int, sts_expire: int) -> None:
  229. """更新 OAuth 令牌并写回配置文件"""
  230. def _find_source_oauth_profile(config: dict, profile_name: str) -> dict:
  231. profiles = config.get('profiles', [])
  232. profile = next((p for p in profiles if p.get('name') == profile_name), None)
  233. if not profile:
  234. raise CredentialException(f"unable to get profile with name '{profile_name}' from cli credentials file.")
  235. if profile.get('mode') == 'OAuth':
  236. return profile
  237. else:
  238. source_profile = profile.get('source_profile')
  239. if source_profile:
  240. return _find_source_oauth_profile(config, source_profile)
  241. raise CredentialException(f"unable to get OAuth profile with name '{profile_name}' from cli credentials file.")
  242. with self._file_lock:
  243. try:
  244. # 读取现有配置
  245. config = _load_config(self._profile_file)
  246. # 找到当前 profile 并更新 OAuth 令牌
  247. profile_name = self._profile_name or config.get('current')
  248. if not profile_name:
  249. raise CredentialException(f"unable to get profile to updated.")
  250. source_profile = _find_source_oauth_profile(config, profile_name)
  251. # 更新 OAuth 令牌
  252. source_profile['oauth_refresh_token'] = refresh_token
  253. source_profile['oauth_access_token'] = access_token
  254. source_profile['oauth_access_token_expire'] = access_token_expire
  255. # 更新 STS 凭据
  256. source_profile['access_key_id'] = access_key
  257. source_profile['access_key_secret'] = secret
  258. source_profile['sts_token'] = security_token
  259. source_profile['sts_expiration'] = sts_expire
  260. self._write_configuration_to_file_with_lock(self._profile_file, config)
  261. except Exception as e:
  262. raise CredentialException(f"failed to update OAuth tokens in config file: {e}")
  263. def _write_configuration_to_file(self, config_path: str, config: Dict) -> None:
  264. """将配置写入文件,使用原子写入确保数据完整性。
  265. 使用 os.replace 而非 os.rename:Windows 上 rename 无法覆盖已存在目标文件
  266. (WinError 183),而 os.replace 提供跨平台的覆盖语义。
  267. """
  268. # 获取原文件权限(如果存在)
  269. file_mode = 0o644
  270. if os.path.exists(config_path):
  271. file_mode = os.stat(config_path).st_mode
  272. # 创建唯一临时文件
  273. import time
  274. temp_file = config_path + '.tmp-' + str(int(time.time() * 1000000)) # 微秒级时间戳
  275. try:
  276. # 写入临时文件
  277. self._write_config_file(temp_file, file_mode, config)
  278. # 原子替换(Windows 上可覆盖已存在的 config_path)
  279. os.replace(temp_file, config_path)
  280. except Exception:
  281. if os.path.exists(temp_file):
  282. try:
  283. os.remove(temp_file)
  284. except OSError:
  285. pass
  286. raise
  287. def _write_config_file(self, filename: str, file_mode: int, config: Dict) -> None:
  288. try:
  289. with open(filename, 'w', encoding='utf-8') as f:
  290. json.dump(config, f, indent=4, ensure_ascii=False)
  291. # 设置文件权限
  292. os.chmod(filename, file_mode)
  293. except Exception as e:
  294. raise CredentialException(f"Failed to write config file: {e}")
  295. def _write_configuration_to_file_with_lock(self, config_path: str, config: Dict) -> None:
  296. """使用操作系统级别的文件锁写入配置文件。
  297. Windows 上目标文件被打开锁定时无法 rename/replace,因此在锁内直接原地写入;
  298. 其他平台使用临时文件 + os.replace 原子覆盖。
  299. """
  300. # 获取原文件权限(如果存在)
  301. file_mode = 0o644
  302. if os.path.exists(config_path):
  303. file_mode = os.stat(config_path).st_mode
  304. # 确保文件存在
  305. if not os.path.exists(config_path):
  306. # 创建空文件
  307. with open(config_path, 'w') as f:
  308. json.dump({}, f)
  309. # 打开文件用于锁定
  310. with open(config_path, 'r+') as f:
  311. # 获取独占锁(阻塞其他进程)
  312. if HAS_MSVCRT:
  313. # Windows使用msvcrt
  314. msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1)
  315. elif HAS_FCNTL:
  316. # Unix/Linux使用fcntl
  317. fcntl.flock(f.fileno(), fcntl.LOCK_EX)
  318. # 如果都不支持,则跳过文件锁(仅进程内保护)
  319. try:
  320. if platform.system() == 'Windows':
  321. # Windows 下目标文件处于打开锁定状态时无法 replace,改为原地写入
  322. f.seek(0)
  323. f.truncate() # 清空文件内容
  324. json.dump(config, f, indent=4, ensure_ascii=False)
  325. f.flush()
  326. os.fsync(f.fileno())
  327. else:
  328. # 其他环境使用临时文件 + os.replace(在文件锁内部进行原子操作)
  329. import time
  330. temp_file = config_path + '.tmp-' + str(int(time.time() * 1000000))
  331. self._write_config_file(temp_file, file_mode, config)
  332. os.replace(temp_file, config_path)
  333. finally:
  334. # 释放锁
  335. try:
  336. if HAS_MSVCRT:
  337. msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
  338. elif HAS_FCNTL:
  339. fcntl.flock(f.fileno(), fcntl.LOCK_UN)
  340. except (OSError, PermissionError):
  341. # 在Windows下,如果文件被重命名,文件句柄可能已经无效
  342. # 这种情况下锁会自动释放,所以忽略错误
  343. pass
  344. def _get_oauth_token_update_callback(self) -> OAuthTokenUpdateCallback:
  345. """获取 OAuth 令牌更新回调函数"""
  346. return lambda refresh_token, access_token, access_key, secret, security_token, access_token_expire, sts_expire: self._update_oauth_tokens(
  347. refresh_token, access_token, access_key, secret, security_token, access_token_expire, sts_expire
  348. )
  349. def _update_external_credentials(self, access_key: str, secret: str,
  350. security_token: str, expiration: int) -> None:
  351. """更新 External 凭证并写回配置文件"""
  352. def _find_source_external_profile(config: dict, profile_name: str) -> dict:
  353. profiles = config.get('profiles', [])
  354. profile = next((p for p in profiles if p.get('name') == profile_name), None)
  355. if not profile:
  356. raise CredentialException(f"unable to get profile with name '{profile_name}' from cli credentials file.")
  357. if profile.get('mode') == 'External':
  358. return profile
  359. source_profile = profile.get('source_profile')
  360. if source_profile:
  361. return _find_source_external_profile(config, source_profile)
  362. raise CredentialException(f"unable to get External profile with name '{profile_name}' from cli credentials file.")
  363. with self._file_lock:
  364. try:
  365. config = _load_config(self._profile_file)
  366. profile_name = self._profile_name or config.get('current')
  367. if not profile_name:
  368. raise CredentialException(f"unable to get profile to updated.")
  369. source_profile = _find_source_external_profile(config, profile_name)
  370. source_profile['access_key_id'] = access_key
  371. source_profile['access_key_secret'] = secret
  372. source_profile['sts_token'] = security_token
  373. source_profile['sts_expiration'] = expiration
  374. self._write_configuration_to_file_with_lock(self._profile_file, config)
  375. except Exception as e:
  376. raise CredentialException(f"failed to update External credentials in config file: {e}")
  377. def _get_external_credential_update_callback(self) -> ExternalCredentialUpdateCallback:
  378. """获取 External 凭证更新回调函数"""
  379. return lambda access_key, secret, security_token, expiration: self._update_external_credentials(
  380. access_key, secret, security_token, expiration
  381. )
  382. async def _write_configuration_to_file_async(self, config_path: str, config: Dict) -> None:
  383. """异步将配置写入文件,使用原子写入确保数据完整性。
  384. 使用 os.replace 而非 os.rename,确保 Windows 上可覆盖已存在文件。
  385. """
  386. # 获取原文件权限(如果存在)
  387. file_mode = 0o644
  388. if os.path.exists(config_path):
  389. file_mode = os.stat(config_path).st_mode
  390. # 创建唯一临时文件
  391. import time
  392. temp_file = config_path + '.tmp-' + str(int(time.time() * 1000000)) # 微秒级时间戳
  393. try:
  394. # 异步写入临时文件
  395. await self._write_config_file_async(temp_file, file_mode, config)
  396. # 原子替换(Windows 上可覆盖已存在的 config_path)
  397. os.replace(temp_file, config_path)
  398. except Exception:
  399. if os.path.exists(temp_file):
  400. try:
  401. os.remove(temp_file)
  402. except OSError:
  403. pass
  404. raise
  405. async def _write_config_file_async(self, filename: str, file_mode: int, config: Dict) -> None:
  406. try:
  407. async with aiofiles.open(filename, 'w', encoding='utf-8') as f:
  408. await f.write(json.dumps(config, indent=4, ensure_ascii=False))
  409. # 设置文件权限
  410. os.chmod(filename, file_mode)
  411. except Exception as e:
  412. raise CredentialException(f"Failed to write config file: {e}")
  413. async def _write_configuration_to_file_with_lock_async(self, config_path: str, config: Dict) -> None:
  414. """异步使用操作系统级别的文件锁写入配置文件。
  415. Windows 上目标文件被打开锁定时无法 rename/replace,因此在锁内直接原地写入;
  416. 其他平台使用临时文件 + os.replace 原子覆盖。
  417. """
  418. # 获取原文件权限(如果存在)
  419. file_mode = 0o644
  420. if os.path.exists(config_path):
  421. file_mode = os.stat(config_path).st_mode
  422. # 确保文件存在
  423. if not os.path.exists(config_path):
  424. # 创建空文件
  425. with open(config_path, 'w') as f:
  426. json.dump({}, f)
  427. # 打开文件用于锁定
  428. with open(config_path, 'r+') as f:
  429. # 获取独占锁(阻塞其他进程)
  430. if HAS_MSVCRT:
  431. # Windows使用msvcrt
  432. msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1)
  433. elif HAS_FCNTL:
  434. # Unix/Linux使用fcntl
  435. fcntl.flock(f.fileno(), fcntl.LOCK_EX)
  436. # 如果都不支持,则跳过文件锁(仅进程内保护)
  437. try:
  438. if platform.system() == 'Windows':
  439. # Windows 下目标文件处于打开锁定状态时无法 replace,改为原地写入
  440. f.seek(0)
  441. f.truncate() # 清空文件内容
  442. json.dump(config, f, indent=4, ensure_ascii=False)
  443. f.flush()
  444. os.fsync(f.fileno())
  445. else:
  446. # 其他环境使用临时文件 + os.replace(在文件锁内部进行原子操作)
  447. import time
  448. temp_file = config_path + '.tmp-' + str(int(time.time() * 1000000))
  449. await self._write_config_file_async(temp_file, file_mode, config)
  450. os.replace(temp_file, config_path)
  451. finally:
  452. # 释放锁
  453. try:
  454. if HAS_MSVCRT:
  455. msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
  456. elif HAS_FCNTL:
  457. fcntl.flock(f.fileno(), fcntl.LOCK_UN)
  458. except (OSError, PermissionError):
  459. # 在Windows下,如果文件被重命名,文件句柄可能已经无效
  460. # 这种情况下锁会自动释放,所以忽略错误
  461. pass
  462. async def _update_oauth_tokens_async(self, refresh_token: str, access_token: str, access_key: str, secret: str,
  463. security_token: str, access_token_expire: int, sts_expire: int) -> None:
  464. """异步更新 OAuth 令牌并写回配置文件"""
  465. def _find_source_oauth_profile(config: dict, profile_name: str) -> dict:
  466. profiles = config.get('profiles', [])
  467. profile = next((p for p in profiles if p.get('name') == profile_name), None)
  468. if not profile:
  469. raise CredentialException(f"unable to get profile with name '{profile_name}' from cli credentials file.")
  470. if profile.get('mode') == 'OAuth':
  471. return profile
  472. else:
  473. source_profile = profile.get('source_profile')
  474. if source_profile:
  475. return _find_source_oauth_profile(config, source_profile)
  476. raise CredentialException(f"unable to get OAuth profile with name '{profile_name}' from cli credentials file.")
  477. with self._file_lock:
  478. try:
  479. # 读取现有配置
  480. config = await _load_config_async(self._profile_file)
  481. # 找到当前 profile 并更新 OAuth 令牌
  482. profile_name = self._profile_name or config.get('current')
  483. if not profile_name:
  484. raise CredentialException(f"unable to get profile to updated.")
  485. source_profile = _find_source_oauth_profile(config, profile_name)
  486. # 更新 OAuth 令牌
  487. source_profile['oauth_refresh_token'] = refresh_token
  488. source_profile['oauth_access_token'] = access_token
  489. source_profile['oauth_access_token_expire'] = access_token_expire
  490. # 更新 STS 凭据
  491. source_profile['access_key_id'] = access_key
  492. source_profile['access_key_secret'] = secret
  493. source_profile['sts_token'] = security_token
  494. source_profile['sts_expiration'] = sts_expire
  495. await self._write_configuration_to_file_with_lock_async(self._profile_file, config)
  496. except Exception as e:
  497. raise CredentialException(f"failed to update OAuth tokens in config file: {e}")
  498. def _get_oauth_token_update_callback_async(self) -> OAuthTokenUpdateCallbackAsync:
  499. """获取异步 OAuth 令牌更新回调函数"""
  500. return lambda refresh_token, access_token, access_key, secret, security_token, access_token_expire, sts_expire: self._update_oauth_tokens_async(
  501. refresh_token, access_token, access_key, secret, security_token, access_token_expire, sts_expire
  502. )
  503. async def _update_external_credentials_async(self, access_key: str, secret: str,
  504. security_token: str, expiration: int) -> None:
  505. """异步更新 External 凭证并写回配置文件"""
  506. def _find_source_external_profile(config: dict, profile_name: str) -> dict:
  507. profiles = config.get('profiles', [])
  508. profile = next((p for p in profiles if p.get('name') == profile_name), None)
  509. if not profile:
  510. raise CredentialException(f"unable to get profile with name '{profile_name}' from cli credentials file.")
  511. if profile.get('mode') == 'External':
  512. return profile
  513. source_profile = profile.get('source_profile')
  514. if source_profile:
  515. return _find_source_external_profile(config, source_profile)
  516. raise CredentialException(f"unable to get External profile with name '{profile_name}' from cli credentials file.")
  517. with self._file_lock:
  518. try:
  519. config = await _load_config_async(self._profile_file)
  520. profile_name = self._profile_name or config.get('current')
  521. if not profile_name:
  522. raise CredentialException(f"unable to get profile to updated.")
  523. source_profile = _find_source_external_profile(config, profile_name)
  524. source_profile['access_key_id'] = access_key
  525. source_profile['access_key_secret'] = secret
  526. source_profile['sts_token'] = security_token
  527. source_profile['sts_expiration'] = expiration
  528. await self._write_configuration_to_file_with_lock_async(self._profile_file, config)
  529. except Exception as e:
  530. raise CredentialException(f"failed to update External credentials in config file: {e}")
  531. def _get_external_credential_update_callback_async(self) -> ExternalCredentialUpdateCallbackAsync:
  532. """获取异步 External 凭证更新回调函数"""
  533. return lambda access_key, secret, security_token, expiration: self._update_external_credentials_async(
  534. access_key, secret, security_token, expiration
  535. )