Skip to content

Commit

Permalink
Add cli profile support
Browse files Browse the repository at this point in the history
  • Loading branch information
JacksonTian committed Aug 15, 2024
1 parent 18b4bd8 commit 59ef8f9
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 6 deletions.
167 changes: 167 additions & 0 deletions sdk/auth/credentials/cli_profile_credentials_provider.go
Original file line number Diff line number Diff line change
@@ -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)

Check failure on line 66 in sdk/auth/credentials/cli_profile_credentials_provider.go

View workflow job for this annotation

GitHub Actions / build (1.13)

undefined: os.ReadFile

Check failure on line 66 in sdk/auth/credentials/cli_profile_credentials_provider.go

View workflow job for this annotation

GitHub Actions / build (1.14)

undefined: os.ReadFile

Check failure on line 66 in sdk/auth/credentials/cli_profile_credentials_provider.go

View workflow job for this annotation

GitHub Actions / build (1.15)

undefined: os.ReadFile
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()
}
192 changes: 192 additions & 0 deletions sdk/auth/credentials/cli_profile_credentials_provider_test.go
Original file line number Diff line number Diff line change
@@ -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")

Check failure on line 14 in sdk/auth/credentials/cli_profile_credentials_provider_test.go

View workflow job for this annotation

GitHub Actions / build (1.13)

undefined: Memory

Check failure on line 14 in sdk/auth/credentials/cli_profile_credentials_provider_test.go

View workflow job for this annotation

GitHub Actions / build (1.14)

undefined: Memory

Check failure on line 14 in sdk/auth/credentials/cli_profile_credentials_provider_test.go

View workflow job for this annotation

GitHub Actions / build (1.15)

undefined: Memory

Check failure on line 14 in sdk/auth/credentials/cli_profile_credentials_provider_test.go

View workflow job for this annotation

GitHub Actions / build (1.16)

undefined: Memory

Check failure on line 14 in sdk/auth/credentials/cli_profile_credentials_provider_test.go

View workflow job for this annotation

GitHub Actions / build (1.17)

undefined: Memory

Check failure on line 14 in sdk/auth/credentials/cli_profile_credentials_provider_test.go

View workflow job for this annotation

GitHub Actions / build (1.18)

undefined: Memory

Check failure on line 14 in sdk/auth/credentials/cli_profile_credentials_provider_test.go

View workflow job for this annotation

GitHub Actions / build (1.19)

undefined: Memory

Check failure on line 14 in sdk/auth/credentials/cli_profile_credentials_provider_test.go

View workflow job for this annotation

GitHub Actions / build (1.20)

undefined: Memory
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'")
}
8 changes: 4 additions & 4 deletions sdk/auth/credentials/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand Down Expand Up @@ -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").
Expand Down Expand Up @@ -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").
Expand Down Expand Up @@ -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").
Expand Down
Loading

0 comments on commit 59ef8f9

Please sign in to comment.