From 59ef8f9511d877039eaa6c8d68ef127fb05a6580 Mon Sep 17 00:00:00 2001 From: Jackson Tian Date: Wed, 14 Aug 2024 16:45:55 +0800 Subject: [PATCH] Add cli profile support --- .../cli_profile_credentials_provider.go | 167 +++++++++++++++ .../cli_profile_credentials_provider_test.go | 192 ++++++++++++++++++ sdk/auth/credentials/credentials_test.go | 8 +- .../credentials/fixtures/.aliyun/config.json | 44 ++++ .../fixtures/invalid_cli_config.json | 1 + .../credentials/fixtures/mock_cli_config.json | 17 ++ .../fixtures/mock_empty_cli_config.json | 1 + .../credentials/{ => fixtures}/mock_oidctoken | 0 sdk/auth/credentials/provider/env_test.go | 2 +- .../provider/instance_credentials_test.go | 1 - sdk/internal/path.go | 16 ++ sdk/internal/path_test.go | 15 ++ 12 files changed, 458 insertions(+), 6 deletions(-) create mode 100644 sdk/auth/credentials/cli_profile_credentials_provider.go create mode 100644 sdk/auth/credentials/cli_profile_credentials_provider_test.go create mode 100644 sdk/auth/credentials/fixtures/.aliyun/config.json create mode 100644 sdk/auth/credentials/fixtures/invalid_cli_config.json create mode 100644 sdk/auth/credentials/fixtures/mock_cli_config.json create mode 100644 sdk/auth/credentials/fixtures/mock_empty_cli_config.json rename sdk/auth/credentials/{ => fixtures}/mock_oidctoken (100%) create mode 100644 sdk/internal/path.go create mode 100644 sdk/internal/path_test.go diff --git a/sdk/auth/credentials/cli_profile_credentials_provider.go b/sdk/auth/credentials/cli_profile_credentials_provider.go new file mode 100644 index 0000000000..4661582689 --- /dev/null +++ b/sdk/auth/credentials/cli_profile_credentials_provider.go @@ -0,0 +1,167 @@ +package credentials + +import ( + "encoding/json" + "fmt" + "os" + "path" + + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/internal" +) + +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 := os.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 = NewStaticAKCredentialsProvider(p.AccessKeyID, p.AccessKeySecret) + case "RamRoleArn": + previousProvider := NewStaticAKCredentialsProvider(p.AccessKeyID, p.AccessKeySecret) + credentialsProvider, err = NewRAMRoleARNCredentialsProvider(previousProvider, p.RoleArn, p.RoleSessionName, p.DurationSeconds, "", p.StsRegion, "") + case "EcsRamRole": + credentialsProvider = NewECSRAMRoleCredentialsProvider(p.RoleName) + case "OIDC": + credentialsProvider, err = NewOIDCCredentialsProviderBuilder(). + WithOIDCTokenFilePath(p.OIDCTokenFile). + WithOIDCProviderARN(p.OIDCProviderARN). + WithRoleArn(p.RoleArn). + WithStsRegion(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 = NewRAMRoleARNCredentialsProvider(previousProvider, p.RoleArn, p.RoleSessionName, p.DurationSeconds, "", p.StsRegion, "") + default: + err = fmt.Errorf("unsupported profile mode '%s'", p.Mode) + } + + return +} + +// 默认设置为 GetHomePath,测试时便于 mock +var getHomePath = internal.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 + } + } + + return provider.innerProvider.GetCredentials() +} diff --git a/sdk/auth/credentials/cli_profile_credentials_provider_test.go b/sdk/auth/credentials/cli_profile_credentials_provider_test.go new file mode 100644 index 0000000000..39b73308f3 --- /dev/null +++ b/sdk/auth/credentials/cli_profile_credentials_provider_test.go @@ -0,0 +1,192 @@ +package credentials + +import ( + "os" + "path" + "strings" + "testing" + + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/internal" + "github.com/stretchr/testify/assert" +) + +func TestCLIProfileCredentialsProvider(t *testing.T) { + rollback := 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: "EcsRamRole", + Name: "EcsRamRole", + RoleName: "rolename", + }, + { + Mode: "OIDC", + Name: "OIDC", + RoleArn: "role_arn", + OIDCTokenFile: "path/to/oidc/file", + OIDCProviderARN: "provider_arn", + }, + { + Mode: "ChainableRamRoleArn", + Name: "ChainableRamRoleArn", + 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: ""}) + // RamRoleArn + cp, err = provider.getCredentialsProvider(conf, "RamRoleArn") + assert.Nil(t, err) + _, ok = cp.(*RAMRoleARNCredentialsProvider) + assert.True(t, ok) + // 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 = internal.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: "", BearerToken: ""}, cc) + + provider = NewCLIProfileCredentialsProviderBuilder().WithProfileName("inexist").Build() + _, err = provider.GetCredentials() + assert.EqualError(t, err, "unable to get profile with 'inexist'") +} diff --git a/sdk/auth/credentials/credentials_test.go b/sdk/auth/credentials/credentials_test.go index 464ebedb89..bc79a5438c 100644 --- a/sdk/auth/credentials/credentials_test.go +++ b/sdk/auth/credentials/credentials_test.go @@ -887,7 +887,7 @@ func TestOIDCCredentialsProviderGetCredentialsWithError(t *testing.T) { wd, _ := os.Getwd() p, err := NewOIDCCredentialsProviderBuilder(). // read a normal token - WithOIDCTokenFilePath(path.Join(wd, "/mock_oidctoken")). + WithOIDCTokenFilePath(path.Join(wd, "fixtures/mock_oidctoken")). WithOIDCProviderARN("provider-arn"). WithRoleArn("roleArn"). WithRoleSessionName("rsn"). @@ -994,7 +994,7 @@ func TestOIDCCredentialsProvider_getCredentials(t *testing.T) { wd, _ := os.Getwd() p, err = NewOIDCCredentialsProviderBuilder(). // read a normal token - WithOIDCTokenFilePath(path.Join(wd, "/mock_oidctoken")). + WithOIDCTokenFilePath(path.Join(wd, "fixtures/mock_oidctoken")). WithOIDCProviderARN("provider-arn"). WithRoleArn("roleArn"). WithRoleSessionName("rsn"). @@ -1127,7 +1127,7 @@ func TestOIDCCredentialsProvider_getCredentialsWithRequestCheck(t *testing.T) { wd, _ := os.Getwd() p, err := NewOIDCCredentialsProviderBuilder(). // read a normal token - WithOIDCTokenFilePath(path.Join(wd, "/mock_oidctoken")). + WithOIDCTokenFilePath(path.Join(wd, "fixtures/mock_oidctoken")). WithOIDCProviderARN("provider-arn"). WithRoleArn("roleArn"). WithRoleSessionName("rsn"). @@ -1168,7 +1168,7 @@ func TestOIDCCredentialsProviderGetCredentials(t *testing.T) { wd, _ := os.Getwd() p, err := NewOIDCCredentialsProviderBuilder(). // read a normal token - WithOIDCTokenFilePath(path.Join(wd, "/mock_oidctoken")). + WithOIDCTokenFilePath(path.Join(wd, "fixtures/mock_oidctoken")). WithOIDCProviderARN("provider-arn"). WithRoleArn("roleArn"). WithRoleSessionName("rsn"). diff --git a/sdk/auth/credentials/fixtures/.aliyun/config.json b/sdk/auth/credentials/fixtures/.aliyun/config.json new file mode 100644 index 0000000000..bf16c3c5c8 --- /dev/null +++ b/sdk/auth/credentials/fixtures/.aliyun/config.json @@ -0,0 +1,44 @@ +{ + "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": "Unsupported", + "mode": "Unsupported" + } + ] +} \ No newline at end of file diff --git a/sdk/auth/credentials/fixtures/invalid_cli_config.json b/sdk/auth/credentials/fixtures/invalid_cli_config.json new file mode 100644 index 0000000000..e4fd6fad78 --- /dev/null +++ b/sdk/auth/credentials/fixtures/invalid_cli_config.json @@ -0,0 +1 @@ +invalid config \ No newline at end of file diff --git a/sdk/auth/credentials/fixtures/mock_cli_config.json b/sdk/auth/credentials/fixtures/mock_cli_config.json new file mode 100644 index 0000000000..c1b99f4323 --- /dev/null +++ b/sdk/auth/credentials/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/sdk/auth/credentials/fixtures/mock_empty_cli_config.json b/sdk/auth/credentials/fixtures/mock_empty_cli_config.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/sdk/auth/credentials/fixtures/mock_empty_cli_config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/sdk/auth/credentials/mock_oidctoken b/sdk/auth/credentials/fixtures/mock_oidctoken similarity index 100% rename from sdk/auth/credentials/mock_oidctoken rename to sdk/auth/credentials/fixtures/mock_oidctoken diff --git a/sdk/auth/credentials/provider/env_test.go b/sdk/auth/credentials/provider/env_test.go index 52bdfb8505..ab02f4626e 100644 --- a/sdk/auth/credentials/provider/env_test.go +++ b/sdk/auth/credentials/provider/env_test.go @@ -26,5 +26,5 @@ func TestEnvResolve(t *testing.T) { os.Setenv(provider.ENVAccessKeySecret, "AccessKeySecret") c, err = p.Resolve() assert.Nil(t, err) - assert.Equal(t, &credentials.AccessKeyCredential{"AccessKeyId", "AccessKeySecret"}, c) + assert.Equal(t, &credentials.AccessKeyCredential{AccessKeyId: "AccessKeyId", AccessKeySecret: "AccessKeySecret"}, c) } diff --git a/sdk/auth/credentials/provider/instance_credentials_test.go b/sdk/auth/credentials/provider/instance_credentials_test.go index d6f6d39945..31c4b613fc 100644 --- a/sdk/auth/credentials/provider/instance_credentials_test.go +++ b/sdk/auth/credentials/provider/instance_credentials_test.go @@ -136,5 +136,4 @@ func TestInstanceCredentialsProvider(t *testing.T) { c, err = p.Resolve() assert.Nil(t, err) assert.Equal(t, credentials.NewStsTokenCredential("STS.*******", "*******", "SecurityToken"), c) - } diff --git a/sdk/internal/path.go b/sdk/internal/path.go new file mode 100644 index 0000000000..974fa35cb7 --- /dev/null +++ b/sdk/internal/path.go @@ -0,0 +1,16 @@ +package internal + +import ( + "os" + "runtime" +) + +// GetHomePath return home directory according to the system. +// if the environmental virables does not exist, will return empty string +func GetHomePath() string { + if runtime.GOOS == "windows" { + return os.Getenv("USERPROFILE") + } + + return os.Getenv("HOME") +} diff --git a/sdk/internal/path_test.go b/sdk/internal/path_test.go new file mode 100644 index 0000000000..950bbfa2cd --- /dev/null +++ b/sdk/internal/path_test.go @@ -0,0 +1,15 @@ +package internal + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetHomePath(t *testing.T) { + homedir := GetHomePath() + assert.NotEqual(t, "", homedir) + assert.True(t, strings.HasPrefix(homedir, "/")) + assert.False(t, strings.HasSuffix(homedir, "/")) +}