diff --git a/credentials/internal/providers/cli_profile.go b/credentials/internal/providers/cli_profile.go new file mode 100644 index 0000000..c348d5f --- /dev/null +++ b/credentials/internal/providers/cli_profile.go @@ -0,0 +1,206 @@ +package providers + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + + "github.com/aliyun/credentials-go/credentials/internal/utils" +) + +type CLIProfileCredentialsProvider struct { + profileName string + innerProvider CredentialsProvider +} + +type CLIProfileCredentialsProviderBuilder struct { + provider *CLIProfileCredentialsProvider +} + +func (b *CLIProfileCredentialsProviderBuilder) WithProfileName(profileName string) *CLIProfileCredentialsProviderBuilder { + b.provider.profileName = profileName + return b +} + +func (b *CLIProfileCredentialsProviderBuilder) Build() *CLIProfileCredentialsProvider { + // 优先级: + // 1. 使用显示指定的 profileName + // 2. 使用环境变量(ALIBABA_CLOUD_PROFILE)制定的 profileName + // 3. 使用 CLI 配置中的当前 profileName + if b.provider.profileName == "" { + b.provider.profileName = os.Getenv("ALIBABA_CLOUD_PROFILE") + } + + return b.provider +} + +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"` + 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"` + SourceProfile string `json:"source_profile"` + RoleName string `json:"ram_role_name"` + OIDCTokenFile string `json:"oidc_token_file"` + OIDCProviderARN string `json:"oidc_provider_arn"` +} + +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 +} + +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 "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). + 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). + WithDurationSeconds(p.DurationSeconds). + WithRoleSessionName(p.RoleSessionName). + 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). + 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 { + homedir := getHomePath() + if homedir == "" { + err = fmt.Errorf("cannot found home dir") + return + } + + cfgPath := path.Join(homedir, ".aliyun/config.json") + + 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 + } + + cc = &Credentials{ + AccessKeyId: innerCC.AccessKeyId, + AccessKeySecret: innerCC.AccessKeySecret, + SecurityToken: innerCC.SecurityToken, + ProviderName: fmt.Sprintf("%s/%s", provider.GetProviderName(), provider.innerProvider.GetProviderName()), + } + + return +} + +func (provider *CLIProfileCredentialsProvider) GetProviderName() string { + return "cli_profile" +} diff --git a/credentials/internal/providers/cli_profile_test.go b/credentials/internal/providers/cli_profile_test.go new file mode 100644 index 0000000..93c171c --- /dev/null +++ b/credentials/internal/providers/cli_profile_test.go @@ -0,0 +1,205 @@ +package providers + +import ( + "os" + "path" + "strings" + "testing" + + "github.com/aliyun/credentials-go/credentials/internal/utils" + "github.com/stretchr/testify/assert" +) + +func TestCLIProfileCredentialsProvider(t *testing.T) { + rollback := utils.Memory("ALIBABA_CLOUD_PROFILE") + defer rollback() + b := NewCLIProfileCredentialsProviderBuilder().Build() + assert.Equal(t, "", b.profileName) + + // get from env + os.Setenv("ALIBABA_CLOUD_PROFILE", "custom_profile") + b = NewCLIProfileCredentialsProviderBuilder().Build() + assert.Equal(t, "custom_profile", b.profileName) + + b = NewCLIProfileCredentialsProviderBuilder().WithProfileName("profilename").Build() + assert.Equal(t, "profilename", b.profileName) +} + +func Test_configuration(t *testing.T) { + wd, _ := os.Getwd() + _, err := newConfigurationFromPath(path.Join(wd, "fixtures/inexist_cli_config.json")) + assert.NotNil(t, err) + assert.True(t, strings.HasPrefix(err.Error(), "reading aliyun cli config from ")) + + _, err = newConfigurationFromPath(path.Join(wd, "fixtures/invalid_cli_config.json")) + assert.NotNil(t, err) + assert.True(t, strings.HasPrefix(err.Error(), "unmarshal aliyun cli config from ")) + + _, err = newConfigurationFromPath(path.Join(wd, "fixtures/mock_empty_cli_config.json")) + assert.True(t, strings.HasPrefix(err.Error(), "no any configured profiles in ")) + + conf, err := newConfigurationFromPath(path.Join(wd, "fixtures/mock_cli_config.json")) + assert.Nil(t, err) + assert.Equal(t, &configuration{ + Current: "default", + Profiles: []*profile{ + { + Mode: "AK", + Name: "default", + AccessKeyID: "akid", + AccessKeySecret: "secret", + }, + { + Mode: "AK", + Name: "jacksontian", + AccessKeyID: "akid", + AccessKeySecret: "secret", + }, + }, + }, conf) + + _, err = conf.getProfile("inexists") + assert.EqualError(t, err, "unable to get profile with 'inexists'") + + p, err := conf.getProfile("jacksontian") + assert.Nil(t, err) + assert.Equal(t, p.Name, "jacksontian") + assert.Equal(t, p.Mode, "AK") +} + +func TestCLIProfileCredentialsProvider_getCredentialsProvider(t *testing.T) { + conf := &configuration{ + Current: "AK", + Profiles: []*profile{ + { + Mode: "AK", + Name: "AK", + AccessKeyID: "akid", + AccessKeySecret: "secret", + }, + { + Mode: "RamRoleArn", + Name: "RamRoleArn", + AccessKeyID: "akid", + AccessKeySecret: "secret", + RoleArn: "arn", + }, + { + Mode: "RamRoleArn", + Name: "Invalid_RamRoleArn", + }, + { + Mode: "EcsRamRole", + Name: "EcsRamRole", + RoleName: "rolename", + }, + { + Mode: "OIDC", + Name: "OIDC", + RoleArn: "role_arn", + OIDCTokenFile: "path/to/oidc/file", + OIDCProviderARN: "provider_arn", + }, + { + Mode: "ChainableRamRoleArn", + Name: "ChainableRamRoleArn", + RoleArn: "arn", + SourceProfile: "AK", + }, + { + Mode: "ChainableRamRoleArn", + Name: "ChainableRamRoleArn2", + SourceProfile: "InvalidSource", + }, + { + Mode: "Unsupported", + Name: "Unsupported", + }, + }, + } + + provider := NewCLIProfileCredentialsProviderBuilder().Build() + _, err := provider.getCredentialsProvider(conf, "inexist") + assert.EqualError(t, err, "unable to get profile with 'inexist'") + + // AK + cp, err := provider.getCredentialsProvider(conf, "AK") + assert.Nil(t, err) + akcp, ok := cp.(*StaticAKCredentialsProvider) + assert.True(t, ok) + cc, err := akcp.GetCredentials() + assert.Nil(t, err) + assert.Equal(t, cc, &Credentials{AccessKeyId: "akid", AccessKeySecret: "secret", SecurityToken: "", ProviderName: "static_ak"}) + // RamRoleArn + cp, err = provider.getCredentialsProvider(conf, "RamRoleArn") + assert.Nil(t, err) + _, ok = cp.(*RAMRoleARNCredentialsProvider) + assert.True(t, ok) + // RamRoleArn invalid ak + _, err = provider.getCredentialsProvider(conf, "Invalid_RamRoleArn") + assert.EqualError(t, err, "the access key id is empty") + // EcsRamRole + cp, err = provider.getCredentialsProvider(conf, "EcsRamRole") + assert.Nil(t, err) + _, ok = cp.(*ECSRAMRoleCredentialsProvider) + assert.True(t, ok) + // OIDC + cp, err = provider.getCredentialsProvider(conf, "OIDC") + assert.Nil(t, err) + _, ok = cp.(*OIDCCredentialsProvider) + assert.True(t, ok) + + // ChainableRamRoleArn + cp, err = provider.getCredentialsProvider(conf, "ChainableRamRoleArn") + assert.Nil(t, err) + _, ok = cp.(*RAMRoleARNCredentialsProvider) + assert.True(t, ok) + + // ChainableRamRoleArn with invalid source profile + _, err = provider.getCredentialsProvider(conf, "ChainableRamRoleArn2") + assert.EqualError(t, err, "get source profile failed: unable to get profile with 'InvalidSource'") + + // Unsupported + _, err = provider.getCredentialsProvider(conf, "Unsupported") + assert.EqualError(t, err, "unsupported profile mode 'Unsupported'") +} + +func TestCLIProfileCredentialsProvider_GetCredentials(t *testing.T) { + defer func() { + getHomePath = utils.GetHomePath + }() + + getHomePath = func() string { + return "" + } + provider := NewCLIProfileCredentialsProviderBuilder().Build() + _, err := provider.GetCredentials() + assert.EqualError(t, err, "cannot found home dir") + + getHomePath = func() string { + return "/path/invalid/home/dir" + } + provider = NewCLIProfileCredentialsProviderBuilder().Build() + _, err = provider.GetCredentials() + assert.EqualError(t, err, "reading aliyun cli config from '/path/invalid/home/dir/.aliyun/config.json' failed open /path/invalid/home/dir/.aliyun/config.json: no such file or directory") + + getHomePath = func() string { + wd, _ := os.Getwd() + return path.Join(wd, "fixtures") + } + + // get credentials by current profile + provider = NewCLIProfileCredentialsProviderBuilder().Build() + cc, err := provider.GetCredentials() + assert.Nil(t, err) + assert.Equal(t, &Credentials{AccessKeyId: "akid", AccessKeySecret: "secret", SecurityToken: "", ProviderName: "cli_profile/static_ak"}, cc) + + provider = NewCLIProfileCredentialsProviderBuilder().WithProfileName("inexist").Build() + _, err = provider.GetCredentials() + assert.EqualError(t, err, "unable to get profile with 'inexist'") + + // The get_credentials_error profile is invalid + provider = NewCLIProfileCredentialsProviderBuilder().WithProfileName("get_credentials_error").Build() + _, err = provider.GetCredentials() + assert.Contains(t, err.Error(), "InvalidAccessKeyId.NotFound") +} diff --git a/credentials/internal/providers/fixtures/.aliyun/config.json b/credentials/internal/providers/fixtures/.aliyun/config.json new file mode 100644 index 0000000..94d17a5 --- /dev/null +++ b/credentials/internal/providers/fixtures/.aliyun/config.json @@ -0,0 +1,51 @@ +{ + "current": "AK", + "profiles": [ + { + "name": "AK", + "mode": "AK", + "access_key_id": "akid", + "access_key_secret": "secret" + }, + { + "name": "RamRoleArn", + "mode": "RamRoleArn", + "access_key_id": "akid", + "access_key_secret": "secret", + "ram_role_arn": "arn" + }, + { + "name": "EcsRamRole", + "mode": "EcsRamRole", + "ram_role_name": "rolename" + }, + { + "name": "OIDC", + "mode": "OIDC", + "ram_role_arn": "role_arn", + "oidc_token_file": "path/to/oidc/file", + "oidc_provider_arn": "provider_arn" + }, + { + "name": "ChainableRamRoleArn", + "mode": "ChainableRamRoleArn", + "source_profile": "AK" + }, + { + "name": "ChainableRamRoleArn2", + "mode": "ChainableRamRoleArn", + "source_profile": "InvalidSource" + }, + { + "name": "get_credentials_error", + "mode": "RamRoleArn", + "access_key_id": "akid", + "access_key_secret": "secret", + "ram_role_arn": "arn" + }, + { + "name": "Unsupported", + "mode": "Unsupported" + } + ] +} \ No newline at end of file diff --git a/credentials/internal/providers/fixtures/invalid_cli_config.json b/credentials/internal/providers/fixtures/invalid_cli_config.json new file mode 100644 index 0000000..e4fd6fa --- /dev/null +++ b/credentials/internal/providers/fixtures/invalid_cli_config.json @@ -0,0 +1 @@ +invalid config \ No newline at end of file diff --git a/credentials/internal/providers/fixtures/mock_cli_config.json b/credentials/internal/providers/fixtures/mock_cli_config.json new file mode 100644 index 0000000..c1b99f4 --- /dev/null +++ b/credentials/internal/providers/fixtures/mock_cli_config.json @@ -0,0 +1,17 @@ +{ + "current": "default", + "profiles": [ + { + "name": "default", + "mode": "AK", + "access_key_id": "akid", + "access_key_secret": "secret" + }, + { + "name": "jacksontian", + "mode": "AK", + "access_key_id": "akid", + "access_key_secret": "secret" + } + ] +} \ No newline at end of file diff --git a/credentials/internal/providers/fixtures/mock_empty_cli_config.json b/credentials/internal/providers/fixtures/mock_empty_cli_config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/credentials/internal/providers/fixtures/mock_empty_cli_config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/credentials/internal/utils/utils.go b/credentials/internal/utils/utils.go index 3337750..fffee1e 100644 --- a/credentials/internal/utils/utils.go +++ b/credentials/internal/utils/utils.go @@ -16,6 +16,7 @@ import ( "io" mathrand "math/rand" "net/url" + "os" "runtime" "strconv" "sync/atomic" @@ -173,3 +174,31 @@ func GetNonce() (uuidHex string) { h.Write([]byte(msg)) return hex.EncodeToString(h.Sum(nil)) } + +// Get first non-empty value +func GetDefaultString(values ...string) string { + for _, v := range values { + if v != "" { + return v + } + } + + return "" +} + +// set back the memoried enviroment variables +type Rollback func() + +func Memory(keys ...string) Rollback { + // remenber enviroment variables + m := make(map[string]string) + for _, key := range keys { + m[key] = os.Getenv(key) + } + + return func() { + for _, key := range keys { + os.Setenv(key, m[key]) + } + } +} diff --git a/credentials/internal/utils/utils_test.go b/credentials/internal/utils/utils_test.go index 4a88f30..0a1377a 100644 --- a/credentials/internal/utils/utils_test.go +++ b/credentials/internal/utils/utils_test.go @@ -5,6 +5,7 @@ import ( "crypto/rsa" "errors" "io" + "os" "regexp" "testing" @@ -150,3 +151,23 @@ bLbtzL2MbwbXlbOztF7ssgzUWAHgKI6hK3g0LhsqBuo3jzmSVO43giZvAkEA08Nm vftlY0Hs1vNXcaBgEA==` Sha256WithRsa("source", secret) } + +func TestGetDefaultString(t *testing.T) { + assert.Equal(t, "default", GetDefaultString("", "default")) + assert.Equal(t, "custom", GetDefaultString("custom", "default")) + assert.Equal(t, "", GetDefaultString("", "", "")) +} + +func TestMemoryAndRollback(t *testing.T) { + os.Setenv("test", "old") + rollback := Memory("test") + os.Setenv("test", "new") + rollback() + + assert.Equal(t, "old", os.Getenv("test")) +} + +func TestGetNonce(t *testing.T) { + assert.Equal(t, 32, len(GetNonce())) + assert.NotEqual(t, GetNonce(), GetNonce()) +}