Repository: 99designs/aws-vault (go)

Issue:
source_profile is not working correctly.
- [v] I am using the latest release of AWS Vault
- [v] I have provided my `.aws/config` (redacted if necessary)
- [v] I have provided the debug output using `aws-vault --debug` (redacted if necessary)

It seems that the `source_profile` option is not working in 7.1.2-Homebrew version.
When I use with the `source_profile` with `dev`, the `get-caller-identity` always uses the `default` profile.

AWS CLI is working well, so I hope this feature is working same with the cli in the future.
Thanks.

```
> aws-vault exec test -- aws sts get-caller-identity
{
    "UserId": "ARxxxx:xxxx,
    "Account": "3701xxxx",
    "Arn": "arn:aws:sts::3701xxxx:assumed-role/xxxx"
}

> aws sts get-caller-identity --profile test
{
    "UserId": "ARxxxx:test",
    "Account": "3604xxxx",
    "Arn": "arn:aws:sts::3604xxxx:assumed-role/xxxx"
}

> vi ~/.aws/config
[profile test]
external_id=1d88xxxx
role_arn=arn:aws:iam::3604xxxx:role/xxxx
role_session_name=test
source_profile=dev
region=ap-northeast-2

[profile dev]
sso_session=common
sso_account_id=2160xxxx
sso_role_name=AdministratorAccess
region=ap-northeast-2
output=json

[default]
sso_session=common
sso_account_id=3701xxxx
sso_role_name=AdministratorAccess
region=ap-northeast-2
output=json

[sso-session common]
sso_start_url=https://xxxx.awsapps.com/start
sso_region=ap-northeast-2
sso_registration_scopes=sso:account:access
```

And here is the debug log.
```
aws-vault exec test --debug -- aws sts get-caller-identity
2023/03/20 09:16:32 aws-vault 7.1.2-Homebrew
2023/03/20 09:16:32 Using prompt driver: terminal
2023/03/20 09:16:32 Loading config file /Users/lamanus/.aws/config
2023/03/20 09:16:32 Parsing config file /Users/lamanus/.aws/config
2023/03/20 09:16:32 [keyring] Considering backends: [keychain]
2023/03/20 09:16:32 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2023/03/20 09:16:32 [keyring] Found 7 results
2023/03/20 09:16:32 profile test: using SSO role credentials
2023/03/20 09:16:32 Setting subprocess env: AWS_REGION=ap-northeast-2, AWS_DEFAULT_REGION=ap-northeast-2
2023/03/20 09:16:32 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2023/03/20 09:16:32 [keyring] Found 7 results
2023/03/20 09:16:32 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2023/03/20 09:16:32 [keyring] Found 7 results
2023/03/20 09:16:32 [keyring] Querying keychain for service="aws-vault", account="sso.GetRoleCredentials,dGVzdA,aHR0cHM6Ly9vcHNub3cuYXdzYXBwcy5jb20vc3RhcnQ,-62135596800", keychain="aws-vault.keychain"
2023/03/20 09:16:32 [keyring] No results found
2023/03/20 09:16:32 [keyring] Querying keychain for service="aws-vault", account="oidc:https://xxxx.awsapps.com/start", keychain="aws-vault.keychain"
2023/03/20 09:16:32 [keyring] Found item "aws-vault oidc token for https://xxxx.awsapps.com/start (expires 2023-03-20T17:12:17+09:00)"
2023/03/20 09:16:33 Got credentials ****************U7QJ for SSO role AdministratorAccess (account: 3701xxxx), expires in 11h59m58.598194s
2023/03/20 09:16:33 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2023/03/20 09:16:33 [keyring] Found 7 results
2023/03/20 09:16:33 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2023/03/20 09:16:33 [keyring] Found 7 results
2023/03/20 09:16:33 [keyring] Checking keychain status
2023/03/20 09:16:33 [keyring] Keychain status returned nil, keychain exists
2023/03/20 09:16:33 [keyring] Keychain item trusts keyring
2023/03/20 09:16:33 [keyring] Adding service="aws-vault", label="aws-vault session for test (expires 2023-03-20T21:16:32+09:00)", account="sso.GetRoleCredentials,dGVzdA,aHR0cHM6Ly9vcHNub3cuYXdzYXBwcy5jb20vc3RhcnQ,1679314592", trusted=true to osx keychain "aws-vault.keychain"
2023/03/20 09:16:33 Setting subprocess env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
2023/03/20 09:16:33 Setting subprocess env: AWS_SESSION_TOKEN
2023/03/20 09:16:33 Setting subprocess env: AWS_CREDENTIAL_EXPIRATION
2023/03/20 09:16:33 Exec command aws sts get-caller-identity
2023/03/20 09:16:33 Found executable /opt/homebrew/bin/aws
{
    "UserId": "ARxxxx:xxxx,
    "Account": "3701xxxx",
    "Arn": "arn:aws:sts::3701xxxx:assumed-role/xxxx"
}
```

Pull request:
Prioritise source_profile over sso config
Prioritise the `source_profile` over sso config.

Fixes #1195

Patch:
diff --git a/vault/vault.go b/vault/vault.go
index 9f3d4a6..9fd27fe 100644
--- a/vault/vault.go
+++ b/vault/vault.go
@@ -41,6 +41,11 @@ func FormatKeyForDisplay(k string) string {
 	return fmt.Sprintf("****************%s", k[len(k)-4:])
 }
 
+func isMasterCredentialsProvider(credsProvider aws.CredentialsProvider) bool {
+	_, ok := credsProvider.(*KeyringProvider)
+	return ok
+}
+
 // NewMasterCredentialsProvider creates a provider for the master credentials
 func NewMasterCredentialsProvider(k *CredentialKeyring, credentialsName string) *KeyringProvider {
 	return &KeyringProvider{k, credentialsName}
@@ -243,52 +248,60 @@ func (t *TempCredentialsCreator) getSourceCreds(config *ProfileConfig, hasStored
 	return nil, fmt.Errorf("profile %s: credentials missing", config.ProfileName)
 }
 
-func (t *TempCredentialsCreator) GetProviderForProfile(config *ProfileConfig) (aws.CredentialsProvider, error) {
-	hasStoredCredentials, err := t.Keyring.Has(config.ProfileName)
+func (t *TempCredentialsCreator) getSourceCredWithSession(config *ProfileConfig, hasStoredCredentials bool) (sourcecredsProvider aws.CredentialsProvider, err error) {
+	sourcecredsProvider, err = t.getSourceCreds(config, hasStoredCredentials)
 	if err != nil {
 		return nil, err
 	}
 
-	if !hasStoredCredentials {
-		if config.HasSSOStartURL() {
-			log.Printf("profile %s: using SSO role credentials", config.ProfileName)
-			return NewSSORoleCredentialsProvider(t.Keyring.Keyring, config, !t.DisableCache)
-		}
-
-		if config.HasWebIdentity() {
-			log.Printf("profile %s: using web identity", config.ProfileName)
-			return NewAssumeRoleWithWebIdentityProvider(t.Keyring.Keyring, config, !t.DisableCache)
+	if config.HasRole() {
+		isMfaChained := config.MfaSerial != "" && config.MfaSerial == t.chainedMfa
+		if isMfaChained {
+			config.MfaSerial = ""
 		}
+		log.Printf("profile %s: using AssumeRole %s", config.ProfileName, mfaDetails(isMfaChained, config))
+		return NewAssumeRoleProvider(sourcecredsProvider, t.Keyring.Keyring, config, !t.DisableCache)
+	}
 
-		if config.HasCredentialProcess() {
-			log.Printf("profile %s: using credential process", config.ProfileName)
-			return NewCredentialProcessProvider(t.Keyring.Keyring, config, !t.DisableCache)
+	if isMasterCredentialsProvider(sourcecredsProvider) {
+		canUseGetSessionToken, reason := t.canUseGetSessionToken(config)
+		if canUseGetSessionToken {
+			t.chainedMfa = config.MfaSerial
+			log.Printf("profile %s: using GetSessionToken %s", config.ProfileName, mfaDetails(false, config))
+			return NewSessionTokenProvider(sourcecredsProvider, t.Keyring.Keyring, config, !t.DisableCache)
 		}
+		log.Printf("profile %s: skipping GetSessionToken because %s", config.ProfileName, reason)
 	}
 
-	sourcecredsProvider, err := t.getSourceCreds(config, hasStoredCredentials)
+	return sourcecredsProvider, nil
+}
+
+func (t *TempCredentialsCreator) GetProviderForProfile(config *ProfileConfig) (aws.CredentialsProvider, error) {
+	hasStoredCredentials, err := t.Keyring.Has(config.ProfileName)
 	if err != nil {
 		return nil, err
 	}
 
-	if config.HasRole() {
-		isMfaChained := config.MfaSerial != "" && config.MfaSerial == t.chainedMfa
-		if isMfaChained {
-			config.MfaSerial = ""
-		}
-		log.Printf("profile %s: using AssumeRole %s", config.ProfileName, mfaDetails(isMfaChained, config))
-		return NewAssumeRoleProvider(sourcecredsProvider, t.Keyring.Keyring, config, !t.DisableCache)
+	if hasStoredCredentials || config.HasSourceProfile() {
+		return t.getSourceCredWithSession(config, hasStoredCredentials)
 	}
 
-	canUseGetSessionToken, reason := t.canUseGetSessionToken(config)
-	if canUseGetSessionToken {
-		t.chainedMfa = config.MfaSerial
-		log.Printf("profile %s: using GetSessionToken %s", config.ProfileName, mfaDetails(false, config))
-		return NewSessionTokenProvider(sourcecredsProvider, t.Keyring.Keyring, config, !t.DisableCache)
+	if config.HasSSOStartURL() {
+		log.Printf("profile %s: using SSO role credentials", config.ProfileName)
+		return NewSSORoleCredentialsProvider(t.Keyring.Keyring, config, !t.DisableCache)
 	}
 
-	log.Printf("profile %s: skipping GetSessionToken because %s", config.ProfileName, reason)
-	return sourcecredsProvider, nil
+	if config.HasWebIdentity() {
+		log.Printf("profile %s: using web identity", config.ProfileName)
+		return NewAssumeRoleWithWebIdentityProvider(t.Keyring.Keyring, config, !t.DisableCache)
+	}
+
+	if config.HasCredentialProcess() {
+		log.Printf("profile %s: using credential process", config.ProfileName)
+		return NewCredentialProcessProvider(t.Keyring.Keyring, config, !t.DisableCache)
+	}
+
+	return nil, fmt.Errorf("profile %s: credentials missing", config.ProfileName)
 }
 
 // canUseGetSessionToken determines if GetSessionToken should be used, and if not returns a reason


Test patch:
diff --git a/vault/vault_test.go b/vault/vault_test.go
new file mode 100644
index 0000000..e4c7466
--- /dev/null
+++ b/vault/vault_test.go
@@ -0,0 +1,60 @@
+package vault_test
+
+import (
+	"os"
+	"testing"
+
+	"github.com/99designs/aws-vault/v7/vault"
+	"github.com/99designs/keyring"
+)
+
+func TestIssue1195(t *testing.T) {
+	f := newConfigFile(t, []byte(`
+[profile test]
+source_profile=dev
+region=ap-northeast-2
+
+[profile dev]
+sso_session=common
+sso_account_id=2160xxxx
+sso_role_name=AdministratorAccess
+region=ap-northeast-2
+output=json
+
+[default]
+sso_session=common
+sso_account_id=3701xxxx
+sso_role_name=AdministratorAccess
+region=ap-northeast-2
+output=json
+
+[sso-session common]
+sso_start_url=https://xxxx.awsapps.com/start
+sso_region=ap-northeast-2
+sso_registration_scopes=sso:account:access
+`))
+	defer os.Remove(f)
+	configFile, err := vault.LoadConfig(f)
+	if err != nil {
+		t.Fatal(err)
+	}
+	configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "test"}
+	config, err := configLoader.GetProfileConfig("test")
+	if err != nil {
+		t.Fatalf("Should have found a profile: %v", err)
+	}
+
+	ckr := &vault.CredentialKeyring{Keyring: keyring.NewArrayKeyring([]keyring.Item{})}
+	p, err := vault.NewTempCredentialsProvider(config, ckr, true, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	ssoProvider, ok := p.(*vault.SSORoleCredentialsProvider)
+	if !ok {
+		t.Fatalf("Expected SSORoleCredentialsProvider, got %T", p)
+	}
+	if ssoProvider.AccountID != "2160xxxx" {
+		t.Fatalf("Expected AccountID to be 2160xxxx, got %s", ssoProvider.AccountID)
+	}
+}