cli_profile.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. package providers
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io/ioutil"
  7. "os"
  8. "path"
  9. "strconv"
  10. "strings"
  11. "sync"
  12. "time"
  13. "github.com/aliyun/credentials-go/credentials/internal/utils"
  14. )
  15. type CLIProfileCredentialsProvider struct {
  16. profileFile string
  17. profileName string
  18. innerProvider CredentialsProvider
  19. // 文件锁,用于并发安全
  20. fileMutex sync.RWMutex
  21. }
  22. type CLIProfileCredentialsProviderBuilder struct {
  23. provider *CLIProfileCredentialsProvider
  24. }
  25. func (b *CLIProfileCredentialsProviderBuilder) WithProfileFile(profileFile string) *CLIProfileCredentialsProviderBuilder {
  26. b.provider.profileFile = profileFile
  27. return b
  28. }
  29. func (b *CLIProfileCredentialsProviderBuilder) WithProfileName(profileName string) *CLIProfileCredentialsProviderBuilder {
  30. b.provider.profileName = profileName
  31. return b
  32. }
  33. func (b *CLIProfileCredentialsProviderBuilder) Build() (provider *CLIProfileCredentialsProvider, err error) {
  34. // 优先级:
  35. // 1. 使用显示指定的 profileFile
  36. // 2. 使用环境变量(ALIBABA_CLOUD_CONFIG_FILE)指定的 profileFile
  37. // 3. 兜底使用 path.Join(homeDir, ".aliyun/config") 作为 profileFile
  38. if b.provider.profileFile == "" {
  39. b.provider.profileFile = os.Getenv("ALIBABA_CLOUD_CONFIG_FILE")
  40. }
  41. // 优先级:
  42. // 1. 使用显示指定的 profileName
  43. // 2. 使用环境变量(ALIBABA_CLOUD_PROFILE)制定的 profileName
  44. // 3. 使用 CLI 配置中的当前 profileName
  45. if b.provider.profileName == "" {
  46. b.provider.profileName = os.Getenv("ALIBABA_CLOUD_PROFILE")
  47. }
  48. if strings.ToLower(os.Getenv("ALIBABA_CLOUD_CLI_PROFILE_DISABLED")) == "true" {
  49. err = errors.New("the CLI profile is disabled")
  50. return
  51. }
  52. provider = b.provider
  53. return
  54. }
  55. func NewCLIProfileCredentialsProviderBuilder() *CLIProfileCredentialsProviderBuilder {
  56. return &CLIProfileCredentialsProviderBuilder{
  57. provider: &CLIProfileCredentialsProvider{},
  58. }
  59. }
  60. type profile struct {
  61. Name string `json:"name"`
  62. Mode string `json:"mode"`
  63. AccessKeyID string `json:"access_key_id"`
  64. AccessKeySecret string `json:"access_key_secret"`
  65. SecurityToken string `json:"sts_token"`
  66. RegionID string `json:"region_id"`
  67. RoleArn string `json:"ram_role_arn"`
  68. RoleSessionName string `json:"ram_session_name"`
  69. DurationSeconds int `json:"expired_seconds"`
  70. StsRegion string `json:"sts_region"`
  71. EnableVpc bool `json:"enable_vpc"`
  72. SourceProfile string `json:"source_profile"`
  73. RoleName string `json:"ram_role_name"`
  74. OIDCTokenFile string `json:"oidc_token_file"`
  75. OIDCProviderARN string `json:"oidc_provider_arn"`
  76. Policy string `json:"policy"`
  77. ExternalId string `json:"external_id"`
  78. SignInUrl string `json:"cloud_sso_sign_in_url"`
  79. AccountId string `json:"cloud_sso_account_id"`
  80. AccessConfig string `json:"cloud_sso_access_config"`
  81. AccessToken string `json:"access_token"`
  82. AccessTokenExpire int64 `json:"cloud_sso_access_token_expire"`
  83. OauthSiteType string `json:"oauth_site_type"`
  84. OauthRefreshToken string `json:"oauth_refresh_token"`
  85. OauthAccessToken string `json:"oauth_access_token"`
  86. OauthAccessTokenExpire int64 `json:"oauth_access_token_expire"`
  87. StsExpire int64 `json:"sts_expiration"`
  88. ProcessCommand string `json:"process_command"`
  89. }
  90. type configuration struct {
  91. Current string `json:"current"`
  92. Profiles []*profile `json:"profiles"`
  93. }
  94. func newConfigurationFromPath(cfgPath string) (conf *configuration, err error) {
  95. bytes, err := ioutil.ReadFile(cfgPath)
  96. if err != nil {
  97. err = fmt.Errorf("reading aliyun cli config from '%s' failed %v", cfgPath, err)
  98. return
  99. }
  100. conf = &configuration{}
  101. err = json.Unmarshal(bytes, conf)
  102. if err != nil {
  103. err = fmt.Errorf("unmarshal aliyun cli config from '%s' failed: %s", cfgPath, string(bytes))
  104. return
  105. }
  106. if conf.Profiles == nil || len(conf.Profiles) == 0 {
  107. err = fmt.Errorf("no any configured profiles in '%s'", cfgPath)
  108. return
  109. }
  110. return
  111. }
  112. func (conf *configuration) getProfile(name string) (profile *profile, err error) {
  113. for _, p := range conf.Profiles {
  114. if p.Name == name {
  115. profile = p
  116. return
  117. }
  118. }
  119. err = fmt.Errorf("unable to get profile with '%s'", name)
  120. return
  121. }
  122. var oauthBaseUrlMap = map[string]string{
  123. "CN": "https://oauth.aliyun.com",
  124. "INTL": "https://oauth.alibabacloud.com",
  125. }
  126. var oauthClientMap = map[string]string{
  127. "CN": "4038181954557748008",
  128. "INTL": "4103531455503354461",
  129. }
  130. func (provider *CLIProfileCredentialsProvider) getCredentialsProvider(conf *configuration, profileName string) (credentialsProvider CredentialsProvider, err error) {
  131. p, err := conf.getProfile(profileName)
  132. if err != nil {
  133. return
  134. }
  135. switch p.Mode {
  136. case "AK":
  137. credentialsProvider, err = NewStaticAKCredentialsProviderBuilder().
  138. WithAccessKeyId(p.AccessKeyID).
  139. WithAccessKeySecret(p.AccessKeySecret).
  140. Build()
  141. case "StsToken":
  142. credentialsProvider, err = NewStaticSTSCredentialsProviderBuilder().
  143. WithAccessKeyId(p.AccessKeyID).
  144. WithAccessKeySecret(p.AccessKeySecret).
  145. WithSecurityToken(p.SecurityToken).
  146. Build()
  147. case "RamRoleArn":
  148. previousProvider, err1 := NewStaticAKCredentialsProviderBuilder().
  149. WithAccessKeyId(p.AccessKeyID).
  150. WithAccessKeySecret(p.AccessKeySecret).
  151. Build()
  152. if err1 != nil {
  153. return nil, err1
  154. }
  155. credentialsProvider, err = NewRAMRoleARNCredentialsProviderBuilder().
  156. WithCredentialsProvider(previousProvider).
  157. WithRoleArn(p.RoleArn).
  158. WithRoleSessionName(p.RoleSessionName).
  159. WithDurationSeconds(p.DurationSeconds).
  160. WithStsRegionId(p.StsRegion).
  161. WithEnableVpc(p.EnableVpc).
  162. WithPolicy(p.Policy).
  163. WithExternalId(p.ExternalId).
  164. Build()
  165. case "EcsRamRole":
  166. credentialsProvider, err = NewECSRAMRoleCredentialsProviderBuilder().WithRoleName(p.RoleName).Build()
  167. case "OIDC":
  168. credentialsProvider, err = NewOIDCCredentialsProviderBuilder().
  169. WithOIDCTokenFilePath(p.OIDCTokenFile).
  170. WithOIDCProviderARN(p.OIDCProviderARN).
  171. WithRoleArn(p.RoleArn).
  172. WithStsRegionId(p.StsRegion).
  173. WithEnableVpc(p.EnableVpc).
  174. WithDurationSeconds(p.DurationSeconds).
  175. WithRoleSessionName(p.RoleSessionName).
  176. WithPolicy(p.Policy).
  177. Build()
  178. case "ChainableRamRoleArn":
  179. previousProvider, err1 := provider.getCredentialsProvider(conf, p.SourceProfile)
  180. if err1 != nil {
  181. err = fmt.Errorf("get source profile failed: %s", err1.Error())
  182. return
  183. }
  184. credentialsProvider, err = NewRAMRoleARNCredentialsProviderBuilder().
  185. WithCredentialsProvider(previousProvider).
  186. WithRoleArn(p.RoleArn).
  187. WithRoleSessionName(p.RoleSessionName).
  188. WithDurationSeconds(p.DurationSeconds).
  189. WithStsRegionId(p.StsRegion).
  190. WithEnableVpc(p.EnableVpc).
  191. WithPolicy(p.Policy).
  192. WithExternalId(p.ExternalId).
  193. Build()
  194. case "CloudSSO":
  195. credentialsProvider, err = NewCloudSSOCredentialsProviderBuilder().
  196. WithSignInUrl(p.SignInUrl).
  197. WithAccountId(p.AccountId).
  198. WithAccessConfig(p.AccessConfig).
  199. WithAccessToken(p.AccessToken).
  200. WithAccessTokenExpire(p.AccessTokenExpire).
  201. Build()
  202. case "OAuth":
  203. siteType := strings.ToUpper(p.OauthSiteType)
  204. signInUrl := oauthBaseUrlMap[siteType]
  205. if signInUrl == "" {
  206. err = fmt.Errorf("invalid site type, support CN or INTL")
  207. return
  208. }
  209. clientId := oauthClientMap[siteType]
  210. credentialsProvider, err = NewOAuthCredentialsProviderBuilder().
  211. WithSignInUrl(signInUrl).
  212. WithClientId(clientId).
  213. WithRefreshToken(p.OauthRefreshToken).
  214. WithAccessToken(p.OauthAccessToken).
  215. WithAccessTokenExpire(p.OauthAccessTokenExpire).
  216. WithTokenUpdateCallback(provider.getOAuthTokenUpdateCallback()).
  217. Build()
  218. case "External":
  219. credentialsProvider, err = NewExternalCredentialsProviderBuilder().
  220. WithProcessCommand(p.ProcessCommand).
  221. WithCredentialUpdateCallback(provider.getExternalCredentialUpdateCallback()).
  222. Build()
  223. default:
  224. err = fmt.Errorf("unsupported profile mode '%s'", p.Mode)
  225. }
  226. return
  227. }
  228. // 默认设置为 GetHomePath,测试时便于 mock
  229. var getHomePath = utils.GetHomePath
  230. func (provider *CLIProfileCredentialsProvider) GetCredentials() (cc *Credentials, err error) {
  231. if provider.innerProvider == nil {
  232. cfgPath := provider.profileFile
  233. if cfgPath == "" {
  234. homeDir := getHomePath()
  235. if homeDir == "" {
  236. err = fmt.Errorf("cannot found home dir")
  237. return
  238. }
  239. cfgPath = path.Join(homeDir, ".aliyun/config.json")
  240. provider.profileFile = cfgPath
  241. }
  242. conf, err1 := newConfigurationFromPath(cfgPath)
  243. if err1 != nil {
  244. err = err1
  245. return
  246. }
  247. if provider.profileName == "" {
  248. provider.profileName = conf.Current
  249. }
  250. provider.innerProvider, err = provider.getCredentialsProvider(conf, provider.profileName)
  251. if err != nil {
  252. return
  253. }
  254. }
  255. innerCC, err := provider.innerProvider.GetCredentials()
  256. if err != nil {
  257. return
  258. }
  259. providerName := innerCC.ProviderName
  260. if providerName == "" {
  261. providerName = provider.innerProvider.GetProviderName()
  262. }
  263. cc = &Credentials{
  264. AccessKeyId: innerCC.AccessKeyId,
  265. AccessKeySecret: innerCC.AccessKeySecret,
  266. SecurityToken: innerCC.SecurityToken,
  267. ProviderName: fmt.Sprintf("%s/%s", provider.GetProviderName(), providerName),
  268. }
  269. return
  270. }
  271. func (provider *CLIProfileCredentialsProvider) GetProviderName() string {
  272. return "cli_profile"
  273. }
  274. // findSourceOAuthProfile 递归查找 OAuth source profile
  275. func (conf *configuration) findSourceOAuthProfile(profileName string) (*profile, error) {
  276. profile, err := conf.getProfile(profileName)
  277. if err != nil {
  278. return nil, fmt.Errorf("unable to get profile with name '%s' from cli credentials file: %v", profileName, err)
  279. }
  280. if profile.Mode == "OAuth" {
  281. return profile, nil
  282. }
  283. if profile.SourceProfile != "" {
  284. return conf.findSourceOAuthProfile(profile.SourceProfile)
  285. }
  286. return nil, fmt.Errorf("unable to get OAuth profile with name '%s' from cli credentials file", profileName)
  287. }
  288. // updateOAuthTokens 更新OAuth令牌并写回配置文件
  289. func (provider *CLIProfileCredentialsProvider) updateOAuthTokens(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error {
  290. provider.fileMutex.Lock()
  291. defer provider.fileMutex.Unlock()
  292. cfgPath := provider.profileFile
  293. conf, err := newConfigurationFromPath(cfgPath)
  294. if err != nil {
  295. return fmt.Errorf("failed to read config file: %v", err)
  296. }
  297. profileName := provider.profileName
  298. if profileName == "" {
  299. profileName = conf.Current
  300. }
  301. if profileName == "" {
  302. return fmt.Errorf("unable to get profile to update")
  303. }
  304. // 递归查找真正的 OAuth source profile
  305. sourceProfile, err := conf.findSourceOAuthProfile(profileName)
  306. if err != nil {
  307. return fmt.Errorf("failed to find OAuth source profile: %v", err)
  308. }
  309. // update OAuth tokens
  310. sourceProfile.OauthRefreshToken = refreshToken
  311. sourceProfile.OauthAccessToken = accessToken
  312. sourceProfile.OauthAccessTokenExpire = accessTokenExpire
  313. // update STS credentials
  314. sourceProfile.AccessKeyID = accessKey
  315. sourceProfile.AccessKeySecret = secret
  316. sourceProfile.SecurityToken = securityToken
  317. sourceProfile.StsExpire = stsExpire
  318. // write back with file lock
  319. return provider.writeConfigurationToFileWithLock(cfgPath, conf)
  320. }
  321. // writeConfigurationToFile 将配置写入文件,使用原子写入确保数据完整性
  322. func (provider *CLIProfileCredentialsProvider) writeConfigurationToFile(cfgPath string, conf *configuration) error {
  323. // 获取原文件权限(如果存在)
  324. fileMode := os.FileMode(0644)
  325. if stat, err := os.Stat(cfgPath); err == nil {
  326. fileMode = stat.Mode()
  327. }
  328. // 创建唯一临时文件
  329. tempFile := cfgPath + ".tmp-" + strconv.FormatInt(time.Now().UnixNano(), 10)
  330. // 写入临时文件
  331. err := provider.writeConfigFile(tempFile, fileMode, conf)
  332. if err != nil {
  333. return fmt.Errorf("failed to write temp file: %v", err)
  334. }
  335. // 原子性重命名,确保文件完整性
  336. err = os.Rename(tempFile, cfgPath)
  337. if err != nil {
  338. // 清理临时文件
  339. os.Remove(tempFile)
  340. return fmt.Errorf("failed to rename temp file: %v", err)
  341. }
  342. return nil
  343. }
  344. // writeConfigFile 写入配置文件
  345. func (provider *CLIProfileCredentialsProvider) writeConfigFile(filename string, fileMode os.FileMode, conf *configuration) error {
  346. f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_RDWR, fileMode)
  347. if err != nil {
  348. return fmt.Errorf("failed to create config file: %w", err)
  349. }
  350. defer func() {
  351. closeErr := f.Close()
  352. if err == nil && closeErr != nil {
  353. err = fmt.Errorf("failed to close config file: %w", closeErr)
  354. }
  355. }()
  356. encoder := json.NewEncoder(f)
  357. encoder.SetIndent("", " ")
  358. if err = encoder.Encode(conf); err != nil {
  359. return fmt.Errorf("failed to serialize config: %w", err)
  360. }
  361. return nil
  362. }
  363. // writeConfigurationToFileWithLock 使用操作系统级别的文件锁写入配置文件
  364. func (provider *CLIProfileCredentialsProvider) writeConfigurationToFileWithLock(cfgPath string, conf *configuration) error {
  365. // 获取原文件权限(如果存在)
  366. fileMode := os.FileMode(0644)
  367. if stat, err := os.Stat(cfgPath); err == nil {
  368. fileMode = stat.Mode()
  369. }
  370. // 打开文件用于锁定
  371. file, err := os.OpenFile(cfgPath, os.O_RDWR|os.O_CREATE, fileMode)
  372. if err != nil {
  373. return fmt.Errorf("failed to open config file: %v", err)
  374. }
  375. // 获取独占锁(阻塞其他进程)
  376. err = lockFile(int(file.Fd()))
  377. if err != nil {
  378. file.Close()
  379. return fmt.Errorf("failed to acquire file lock: %v", err)
  380. }
  381. // 创建唯一临时文件
  382. tempFile := cfgPath + ".tmp-" + strconv.FormatInt(time.Now().UnixNano(), 10)
  383. err = provider.writeConfigFile(tempFile, fileMode, conf)
  384. if err != nil {
  385. unlockFile(int(file.Fd()))
  386. file.Close()
  387. return fmt.Errorf("failed to write temp file: %v", err)
  388. }
  389. // 关闭并解锁原文件,以便在Windows上可以重命名
  390. unlockFile(int(file.Fd()))
  391. file.Close()
  392. // 原子性重命名
  393. err = os.Rename(tempFile, cfgPath)
  394. if err != nil {
  395. os.Remove(tempFile)
  396. return fmt.Errorf("failed to rename temp file: %v", err)
  397. }
  398. return nil
  399. }
  400. // getOAuthTokenUpdateCallback 获取OAuth令牌更新回调函数
  401. func (provider *CLIProfileCredentialsProvider) getOAuthTokenUpdateCallback() OAuthTokenUpdateCallback {
  402. return func(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error {
  403. return provider.updateOAuthTokens(refreshToken, accessToken, accessKey, secret, securityToken, accessTokenExpire, stsExpire)
  404. }
  405. }
  406. // getExternalCredentialUpdateCallback 获取External凭证更新回调函数
  407. func (provider *CLIProfileCredentialsProvider) getExternalCredentialUpdateCallback() ExternalCredentialUpdateCallback {
  408. return func(accessKeyId, accessKeySecret, securityToken string, expiration int64) error {
  409. return provider.updateExternalCredentials(accessKeyId, accessKeySecret, securityToken, expiration)
  410. }
  411. }
  412. // updateExternalCredentials 更新External凭证并写回配置文件
  413. func (provider *CLIProfileCredentialsProvider) updateExternalCredentials(accessKeyId, accessKeySecret, securityToken string, expiration int64) error {
  414. provider.fileMutex.Lock()
  415. defer provider.fileMutex.Unlock()
  416. cfgPath := provider.profileFile
  417. conf, err := newConfigurationFromPath(cfgPath)
  418. if err != nil {
  419. return fmt.Errorf("failed to read config file: %v", err)
  420. }
  421. profileName := provider.profileName
  422. profile, err := conf.getProfile(profileName)
  423. if err != nil {
  424. return fmt.Errorf("failed to get profile %s: %v", profileName, err)
  425. }
  426. // update
  427. profile.AccessKeyID = accessKeyId
  428. profile.AccessKeySecret = accessKeySecret
  429. profile.SecurityToken = securityToken
  430. if expiration > 0 {
  431. profile.StsExpire = expiration
  432. }
  433. // write back with file lock
  434. return provider.writeConfigurationToFileWithLock(cfgPath, conf)
  435. }