| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- package providers
- import (
- "encoding/json"
- "errors"
- "fmt"
- "io/ioutil"
- "os"
- "path"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/aliyun/credentials-go/credentials/internal/utils"
- )
- type CLIProfileCredentialsProvider struct {
- profileFile string
- profileName string
- innerProvider CredentialsProvider
- // 文件锁,用于并发安全
- fileMutex sync.RWMutex
- }
- type CLIProfileCredentialsProviderBuilder struct {
- provider *CLIProfileCredentialsProvider
- }
- func (b *CLIProfileCredentialsProviderBuilder) WithProfileFile(profileFile string) *CLIProfileCredentialsProviderBuilder {
- b.provider.profileFile = profileFile
- return b
- }
- func (b *CLIProfileCredentialsProviderBuilder) WithProfileName(profileName string) *CLIProfileCredentialsProviderBuilder {
- b.provider.profileName = profileName
- return b
- }
- func (b *CLIProfileCredentialsProviderBuilder) Build() (provider *CLIProfileCredentialsProvider, err error) {
- // 优先级:
- // 1. 使用显示指定的 profileFile
- // 2. 使用环境变量(ALIBABA_CLOUD_CONFIG_FILE)指定的 profileFile
- // 3. 兜底使用 path.Join(homeDir, ".aliyun/config") 作为 profileFile
- if b.provider.profileFile == "" {
- b.provider.profileFile = os.Getenv("ALIBABA_CLOUD_CONFIG_FILE")
- }
- // 优先级:
- // 1. 使用显示指定的 profileName
- // 2. 使用环境变量(ALIBABA_CLOUD_PROFILE)制定的 profileName
- // 3. 使用 CLI 配置中的当前 profileName
- if b.provider.profileName == "" {
- b.provider.profileName = os.Getenv("ALIBABA_CLOUD_PROFILE")
- }
- if strings.ToLower(os.Getenv("ALIBABA_CLOUD_CLI_PROFILE_DISABLED")) == "true" {
- err = errors.New("the CLI profile is disabled")
- return
- }
- provider = b.provider
- return
- }
- func NewCLIProfileCredentialsProviderBuilder() *CLIProfileCredentialsProviderBuilder {
- return &CLIProfileCredentialsProviderBuilder{
- provider: &CLIProfileCredentialsProvider{},
- }
- }
- type profile struct {
- Name string `json:"name"`
- Mode string `json:"mode"`
- AccessKeyID string `json:"access_key_id"`
- AccessKeySecret string `json:"access_key_secret"`
- SecurityToken string `json:"sts_token"`
- RegionID string `json:"region_id"`
- RoleArn string `json:"ram_role_arn"`
- RoleSessionName string `json:"ram_session_name"`
- DurationSeconds int `json:"expired_seconds"`
- StsRegion string `json:"sts_region"`
- EnableVpc bool `json:"enable_vpc"`
- SourceProfile string `json:"source_profile"`
- RoleName string `json:"ram_role_name"`
- OIDCTokenFile string `json:"oidc_token_file"`
- OIDCProviderARN string `json:"oidc_provider_arn"`
- Policy string `json:"policy"`
- ExternalId string `json:"external_id"`
- SignInUrl string `json:"cloud_sso_sign_in_url"`
- AccountId string `json:"cloud_sso_account_id"`
- AccessConfig string `json:"cloud_sso_access_config"`
- AccessToken string `json:"access_token"`
- AccessTokenExpire int64 `json:"cloud_sso_access_token_expire"`
- OauthSiteType string `json:"oauth_site_type"`
- OauthRefreshToken string `json:"oauth_refresh_token"`
- OauthAccessToken string `json:"oauth_access_token"`
- OauthAccessTokenExpire int64 `json:"oauth_access_token_expire"`
- StsExpire int64 `json:"sts_expiration"`
- ProcessCommand string `json:"process_command"`
- }
- type configuration struct {
- Current string `json:"current"`
- Profiles []*profile `json:"profiles"`
- }
- func newConfigurationFromPath(cfgPath string) (conf *configuration, err error) {
- bytes, err := ioutil.ReadFile(cfgPath)
- if err != nil {
- err = fmt.Errorf("reading aliyun cli config from '%s' failed %v", cfgPath, err)
- return
- }
- conf = &configuration{}
- err = json.Unmarshal(bytes, conf)
- if err != nil {
- err = fmt.Errorf("unmarshal aliyun cli config from '%s' failed: %s", cfgPath, string(bytes))
- return
- }
- if conf.Profiles == nil || len(conf.Profiles) == 0 {
- err = fmt.Errorf("no any configured profiles in '%s'", cfgPath)
- return
- }
- return
- }
- func (conf *configuration) getProfile(name string) (profile *profile, err error) {
- for _, p := range conf.Profiles {
- if p.Name == name {
- profile = p
- return
- }
- }
- err = fmt.Errorf("unable to get profile with '%s'", name)
- return
- }
- var oauthBaseUrlMap = map[string]string{
- "CN": "https://oauth.aliyun.com",
- "INTL": "https://oauth.alibabacloud.com",
- }
- var oauthClientMap = map[string]string{
- "CN": "4038181954557748008",
- "INTL": "4103531455503354461",
- }
- func (provider *CLIProfileCredentialsProvider) getCredentialsProvider(conf *configuration, profileName string) (credentialsProvider CredentialsProvider, err error) {
- p, err := conf.getProfile(profileName)
- if err != nil {
- return
- }
- switch p.Mode {
- case "AK":
- credentialsProvider, err = NewStaticAKCredentialsProviderBuilder().
- WithAccessKeyId(p.AccessKeyID).
- WithAccessKeySecret(p.AccessKeySecret).
- Build()
- case "StsToken":
- credentialsProvider, err = NewStaticSTSCredentialsProviderBuilder().
- WithAccessKeyId(p.AccessKeyID).
- WithAccessKeySecret(p.AccessKeySecret).
- WithSecurityToken(p.SecurityToken).
- Build()
- case "RamRoleArn":
- previousProvider, err1 := NewStaticAKCredentialsProviderBuilder().
- WithAccessKeyId(p.AccessKeyID).
- WithAccessKeySecret(p.AccessKeySecret).
- Build()
- if err1 != nil {
- return nil, err1
- }
- credentialsProvider, err = NewRAMRoleARNCredentialsProviderBuilder().
- WithCredentialsProvider(previousProvider).
- WithRoleArn(p.RoleArn).
- WithRoleSessionName(p.RoleSessionName).
- WithDurationSeconds(p.DurationSeconds).
- WithStsRegionId(p.StsRegion).
- WithEnableVpc(p.EnableVpc).
- WithPolicy(p.Policy).
- WithExternalId(p.ExternalId).
- Build()
- case "EcsRamRole":
- credentialsProvider, err = NewECSRAMRoleCredentialsProviderBuilder().WithRoleName(p.RoleName).Build()
- case "OIDC":
- credentialsProvider, err = NewOIDCCredentialsProviderBuilder().
- WithOIDCTokenFilePath(p.OIDCTokenFile).
- WithOIDCProviderARN(p.OIDCProviderARN).
- WithRoleArn(p.RoleArn).
- WithStsRegionId(p.StsRegion).
- WithEnableVpc(p.EnableVpc).
- WithDurationSeconds(p.DurationSeconds).
- WithRoleSessionName(p.RoleSessionName).
- WithPolicy(p.Policy).
- Build()
- case "ChainableRamRoleArn":
- previousProvider, err1 := provider.getCredentialsProvider(conf, p.SourceProfile)
- if err1 != nil {
- err = fmt.Errorf("get source profile failed: %s", err1.Error())
- return
- }
- credentialsProvider, err = NewRAMRoleARNCredentialsProviderBuilder().
- WithCredentialsProvider(previousProvider).
- WithRoleArn(p.RoleArn).
- WithRoleSessionName(p.RoleSessionName).
- WithDurationSeconds(p.DurationSeconds).
- WithStsRegionId(p.StsRegion).
- WithEnableVpc(p.EnableVpc).
- WithPolicy(p.Policy).
- WithExternalId(p.ExternalId).
- Build()
- case "CloudSSO":
- credentialsProvider, err = NewCloudSSOCredentialsProviderBuilder().
- WithSignInUrl(p.SignInUrl).
- WithAccountId(p.AccountId).
- WithAccessConfig(p.AccessConfig).
- WithAccessToken(p.AccessToken).
- WithAccessTokenExpire(p.AccessTokenExpire).
- Build()
- case "OAuth":
- siteType := strings.ToUpper(p.OauthSiteType)
- signInUrl := oauthBaseUrlMap[siteType]
- if signInUrl == "" {
- err = fmt.Errorf("invalid site type, support CN or INTL")
- return
- }
- clientId := oauthClientMap[siteType]
- credentialsProvider, err = NewOAuthCredentialsProviderBuilder().
- WithSignInUrl(signInUrl).
- WithClientId(clientId).
- WithRefreshToken(p.OauthRefreshToken).
- WithAccessToken(p.OauthAccessToken).
- WithAccessTokenExpire(p.OauthAccessTokenExpire).
- WithTokenUpdateCallback(provider.getOAuthTokenUpdateCallback()).
- Build()
- case "External":
- credentialsProvider, err = NewExternalCredentialsProviderBuilder().
- WithProcessCommand(p.ProcessCommand).
- WithCredentialUpdateCallback(provider.getExternalCredentialUpdateCallback()).
- Build()
- default:
- err = fmt.Errorf("unsupported profile mode '%s'", p.Mode)
- }
- return
- }
- // 默认设置为 GetHomePath,测试时便于 mock
- var getHomePath = utils.GetHomePath
- func (provider *CLIProfileCredentialsProvider) GetCredentials() (cc *Credentials, err error) {
- if provider.innerProvider == nil {
- cfgPath := provider.profileFile
- if cfgPath == "" {
- homeDir := getHomePath()
- if homeDir == "" {
- err = fmt.Errorf("cannot found home dir")
- return
- }
- cfgPath = path.Join(homeDir, ".aliyun/config.json")
- provider.profileFile = cfgPath
- }
- conf, err1 := newConfigurationFromPath(cfgPath)
- if err1 != nil {
- err = err1
- return
- }
- if provider.profileName == "" {
- provider.profileName = conf.Current
- }
- provider.innerProvider, err = provider.getCredentialsProvider(conf, provider.profileName)
- if err != nil {
- return
- }
- }
- innerCC, err := provider.innerProvider.GetCredentials()
- if err != nil {
- return
- }
- providerName := innerCC.ProviderName
- if providerName == "" {
- providerName = provider.innerProvider.GetProviderName()
- }
- cc = &Credentials{
- AccessKeyId: innerCC.AccessKeyId,
- AccessKeySecret: innerCC.AccessKeySecret,
- SecurityToken: innerCC.SecurityToken,
- ProviderName: fmt.Sprintf("%s/%s", provider.GetProviderName(), providerName),
- }
- return
- }
- func (provider *CLIProfileCredentialsProvider) GetProviderName() string {
- return "cli_profile"
- }
- // findSourceOAuthProfile 递归查找 OAuth source profile
- func (conf *configuration) findSourceOAuthProfile(profileName string) (*profile, error) {
- profile, err := conf.getProfile(profileName)
- if err != nil {
- return nil, fmt.Errorf("unable to get profile with name '%s' from cli credentials file: %v", profileName, err)
- }
- if profile.Mode == "OAuth" {
- return profile, nil
- }
- if profile.SourceProfile != "" {
- return conf.findSourceOAuthProfile(profile.SourceProfile)
- }
- return nil, fmt.Errorf("unable to get OAuth profile with name '%s' from cli credentials file", profileName)
- }
- // updateOAuthTokens 更新OAuth令牌并写回配置文件
- func (provider *CLIProfileCredentialsProvider) updateOAuthTokens(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error {
- provider.fileMutex.Lock()
- defer provider.fileMutex.Unlock()
- cfgPath := provider.profileFile
- conf, err := newConfigurationFromPath(cfgPath)
- if err != nil {
- return fmt.Errorf("failed to read config file: %v", err)
- }
- profileName := provider.profileName
- if profileName == "" {
- profileName = conf.Current
- }
- if profileName == "" {
- return fmt.Errorf("unable to get profile to update")
- }
- // 递归查找真正的 OAuth source profile
- sourceProfile, err := conf.findSourceOAuthProfile(profileName)
- if err != nil {
- return fmt.Errorf("failed to find OAuth source profile: %v", err)
- }
- // update OAuth tokens
- sourceProfile.OauthRefreshToken = refreshToken
- sourceProfile.OauthAccessToken = accessToken
- sourceProfile.OauthAccessTokenExpire = accessTokenExpire
- // update STS credentials
- sourceProfile.AccessKeyID = accessKey
- sourceProfile.AccessKeySecret = secret
- sourceProfile.SecurityToken = securityToken
- sourceProfile.StsExpire = stsExpire
- // write back with file lock
- return provider.writeConfigurationToFileWithLock(cfgPath, conf)
- }
- // writeConfigurationToFile 将配置写入文件,使用原子写入确保数据完整性
- func (provider *CLIProfileCredentialsProvider) writeConfigurationToFile(cfgPath string, conf *configuration) error {
- // 获取原文件权限(如果存在)
- fileMode := os.FileMode(0644)
- if stat, err := os.Stat(cfgPath); err == nil {
- fileMode = stat.Mode()
- }
- // 创建唯一临时文件
- tempFile := cfgPath + ".tmp-" + strconv.FormatInt(time.Now().UnixNano(), 10)
- // 写入临时文件
- err := provider.writeConfigFile(tempFile, fileMode, conf)
- if err != nil {
- return fmt.Errorf("failed to write temp file: %v", err)
- }
- // 原子性重命名,确保文件完整性
- err = os.Rename(tempFile, cfgPath)
- if err != nil {
- // 清理临时文件
- os.Remove(tempFile)
- return fmt.Errorf("failed to rename temp file: %v", err)
- }
- return nil
- }
- // writeConfigFile 写入配置文件
- func (provider *CLIProfileCredentialsProvider) writeConfigFile(filename string, fileMode os.FileMode, conf *configuration) error {
- f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_RDWR, fileMode)
- if err != nil {
- return fmt.Errorf("failed to create config file: %w", err)
- }
- defer func() {
- closeErr := f.Close()
- if err == nil && closeErr != nil {
- err = fmt.Errorf("failed to close config file: %w", closeErr)
- }
- }()
- encoder := json.NewEncoder(f)
- encoder.SetIndent("", " ")
- if err = encoder.Encode(conf); err != nil {
- return fmt.Errorf("failed to serialize config: %w", err)
- }
- return nil
- }
- // writeConfigurationToFileWithLock 使用操作系统级别的文件锁写入配置文件
- func (provider *CLIProfileCredentialsProvider) writeConfigurationToFileWithLock(cfgPath string, conf *configuration) error {
- // 获取原文件权限(如果存在)
- fileMode := os.FileMode(0644)
- if stat, err := os.Stat(cfgPath); err == nil {
- fileMode = stat.Mode()
- }
- // 打开文件用于锁定
- file, err := os.OpenFile(cfgPath, os.O_RDWR|os.O_CREATE, fileMode)
- if err != nil {
- return fmt.Errorf("failed to open config file: %v", err)
- }
- // 获取独占锁(阻塞其他进程)
- err = lockFile(int(file.Fd()))
- if err != nil {
- file.Close()
- return fmt.Errorf("failed to acquire file lock: %v", err)
- }
- // 创建唯一临时文件
- tempFile := cfgPath + ".tmp-" + strconv.FormatInt(time.Now().UnixNano(), 10)
- err = provider.writeConfigFile(tempFile, fileMode, conf)
- if err != nil {
- unlockFile(int(file.Fd()))
- file.Close()
- return fmt.Errorf("failed to write temp file: %v", err)
- }
- // 关闭并解锁原文件,以便在Windows上可以重命名
- unlockFile(int(file.Fd()))
- file.Close()
- // 原子性重命名
- err = os.Rename(tempFile, cfgPath)
- if err != nil {
- os.Remove(tempFile)
- return fmt.Errorf("failed to rename temp file: %v", err)
- }
- return nil
- }
- // getOAuthTokenUpdateCallback 获取OAuth令牌更新回调函数
- func (provider *CLIProfileCredentialsProvider) getOAuthTokenUpdateCallback() OAuthTokenUpdateCallback {
- return func(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error {
- return provider.updateOAuthTokens(refreshToken, accessToken, accessKey, secret, securityToken, accessTokenExpire, stsExpire)
- }
- }
- // getExternalCredentialUpdateCallback 获取External凭证更新回调函数
- func (provider *CLIProfileCredentialsProvider) getExternalCredentialUpdateCallback() ExternalCredentialUpdateCallback {
- return func(accessKeyId, accessKeySecret, securityToken string, expiration int64) error {
- return provider.updateExternalCredentials(accessKeyId, accessKeySecret, securityToken, expiration)
- }
- }
- // updateExternalCredentials 更新External凭证并写回配置文件
- func (provider *CLIProfileCredentialsProvider) updateExternalCredentials(accessKeyId, accessKeySecret, securityToken string, expiration int64) error {
- provider.fileMutex.Lock()
- defer provider.fileMutex.Unlock()
- cfgPath := provider.profileFile
- conf, err := newConfigurationFromPath(cfgPath)
- if err != nil {
- return fmt.Errorf("failed to read config file: %v", err)
- }
- profileName := provider.profileName
- profile, err := conf.getProfile(profileName)
- if err != nil {
- return fmt.Errorf("failed to get profile %s: %v", profileName, err)
- }
- // update
- profile.AccessKeyID = accessKeyId
- profile.AccessKeySecret = accessKeySecret
- profile.SecurityToken = securityToken
- if expiration > 0 {
- profile.StsExpire = expiration
- }
- // write back with file lock
- return provider.writeConfigurationToFileWithLock(cfgPath, conf)
- }
|