Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.15
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect
github.com/aws/aws-sdk-go-v2/service/iam v1.19.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/aws/smithy-go v1.13.5
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
Expand Down
122 changes: 122 additions & 0 deletions plugins/aws/access_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,128 @@ func TestSourceProfileLoop(t *testing.T) {
})
}

func TestSTSProvisionerYieldsOnSSOProfile(t *testing.T) {
t.Setenv("AWS_PROFILE", "")
t.Setenv("AWS_DEFAULT_REGION", "")
configPath := filepath.Join(t.TempDir(), "awsConfig")
t.Setenv("AWS_CONFIG_FILE", configPath)

file := ini.Empty()

profileLegacy, err := file.NewSection("profile sso-legacy")
require.NoError(t, err)
_, err = profileLegacy.NewKey("sso_start_url", "https://example.awsapps.com/start")
require.NoError(t, err)
_, err = profileLegacy.NewKey("sso_region", "us-east-1")
require.NoError(t, err)
_, err = profileLegacy.NewKey("sso_account_id", "111111111111")
require.NoError(t, err)
_, err = profileLegacy.NewKey("sso_role_name", "ReadOnly")
require.NoError(t, err)

ssoSession, err := file.NewSection("sso-session corp")
require.NoError(t, err)
_, err = ssoSession.NewKey("sso_start_url", "https://corp.awsapps.com/start")
require.NoError(t, err)
_, err = ssoSession.NewKey("sso_region", "eu-west-1")
require.NoError(t, err)

profileSession, err := file.NewSection("profile sso-new")
require.NoError(t, err)
_, err = profileSession.NewKey("sso_session", "corp")
require.NoError(t, err)
_, err = profileSession.NewKey("sso_account_id", "222222222222")
require.NoError(t, err)
_, err = profileSession.NewKey("sso_role_name", "PowerUser")
require.NoError(t, err)

err = file.SaveTo(configPath)
require.NoError(t, err)

// When the active profile is SSO-configured, the STS provisioner must yield
// silently — no env vars, no error — so the SSO Profile provisioner can run.
plugintest.TestProvisioner(t, STSProvisioner{
profileName: "sso-legacy",
newProviderFactory: func(cacheState sdk.CacheState, cacheOps sdk.CacheOperations, fields map[sdk.FieldName]string) STSProviderFactory {
return &mockProviderManager{}
},
}, map[string]plugintest.ProvisionCase{
"legacy SSO profile yields silently": {
ItemFields: map[sdk.FieldName]string{
fieldname.AccessKeyID: "AKIAHPIZFMD5EEXAMPLE",
fieldname.SecretAccessKey: "lBfKB7P5ScmpxDeRoFLZvhJbqNGPoV0vIEXAMPLE",
},
ExpectedOutput: sdk.ProvisionOutput{},
},
})

plugintest.TestProvisioner(t, STSProvisioner{
profileName: "sso-new",
newProviderFactory: func(cacheState sdk.CacheState, cacheOps sdk.CacheOperations, fields map[sdk.FieldName]string) STSProviderFactory {
return &mockProviderManager{}
},
}, map[string]plugintest.ProvisionCase{
"sso_session profile yields silently": {
ItemFields: map[sdk.FieldName]string{
fieldname.AccessKeyID: "AKIAHPIZFMD5EEXAMPLE",
fieldname.SecretAccessKey: "lBfKB7P5ScmpxDeRoFLZvhJbqNGPoV0vIEXAMPLE",
},
ExpectedOutput: sdk.ProvisionOutput{},
},
})
}

func TestSTSProvisionerErrorsOnSSOSourceProfile(t *testing.T) {
t.Setenv("AWS_PROFILE", "")
t.Setenv("AWS_DEFAULT_REGION", "")
configPath := filepath.Join(t.TempDir(), "awsConfig")
t.Setenv("AWS_CONFIG_FILE", configPath)

file := ini.Empty()

ssoSource, err := file.NewSection("profile sso-src")
require.NoError(t, err)
_, err = ssoSource.NewKey("sso_start_url", "https://example.awsapps.com/start")
require.NoError(t, err)
_, err = ssoSource.NewKey("sso_region", "us-east-1")
require.NoError(t, err)
_, err = ssoSource.NewKey("sso_account_id", "111111111111")
require.NoError(t, err)
_, err = ssoSource.NewKey("sso_role_name", "ReadOnly")
require.NoError(t, err)

chained, err := file.NewSection("profile chained")
require.NoError(t, err)
_, err = chained.NewKey("role_arn", "arn:aws:iam::111111111111:role/Chained")
require.NoError(t, err)
_, err = chained.NewKey("source_profile", "sso-src")
require.NoError(t, err)

err = file.SaveTo(configPath)
require.NoError(t, err)

// A role-chain profile whose source profile is SSO-configured is handled by neither
// provisioner: the top-level profile is not SSO, so the SSO provisioner yields, and the
// source profile has no long-lived keys. The STS provisioner must surface a clear error
// rather than failing later with a misleading missing-access-key message.
plugintest.TestProvisioner(t, STSProvisioner{
profileName: "chained",
newProviderFactory: func(cacheState sdk.CacheState, cacheOps sdk.CacheOperations, fields map[sdk.FieldName]string) STSProviderFactory {
return &mockProviderManager{}
},
}, map[string]plugintest.ProvisionCase{
"SSO source profile in role chain errors clearly": {
ItemFields: map[sdk.FieldName]string{
fieldname.AccessKeyID: "AKIAHPIZFMD5EEXAMPLE",
fieldname.SecretAccessKey: "lBfKB7P5ScmpxDeRoFLZvhJbqNGPoV0vIEXAMPLE",
},
ExpectedOutput: sdk.ProvisionOutput{
Diagnostics: sdk.Diagnostics{Errors: []sdk.Error{{Message: `profile "sso-src" is configured for AWS IAM Identity Center (SSO) and is not yet supported as the source profile of a role chain by the AWS Shell Plugin`}}},
},
},
})
}

func TestResolveLocalAnd1PasswordConfigurations(t *testing.T) {
for _, scenario := range []struct {
description string
Expand Down
32 changes: 32 additions & 0 deletions plugins/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,43 @@ func AWSCLI() schema.Executable {
NeedsAuth: needsauth.IfAll(
needsauth.NotForHelpOrVersion(),
needsauth.NotWithoutArgs(),
// Each entry below intentionally bypasses provisioning. Subcommands not listed here
// (e.g. `aws configure import`, `aws configure export-credentials`) still flow through
// the provisioner because they exchange or mutate credentials and benefit from the
// 1Password-managed surface.
//
// `aws sso login`/`logout` write/erase the very token the SSO Profile provisioner reads
// from disk; provisioning before they run creates a chicken-and-egg failure.
needsauth.NotWhenContainsArgs("sso", "login"),
needsauth.NotWhenContainsArgs("sso", "logout"),
// `aws configure sso` and `aws configure sso-session` set up SSO configuration in
// ~/.aws/config; they touch no AWS APIs and need no provisioned credentials.
needsauth.NotWhenContainsArgs("configure", "sso"),
needsauth.NotWhenContainsArgs("configure", "sso-session"),
// `aws configure list` and `list-profiles` are read-only diagnostic queries against
// the local config; `get` and `set` mutate ~/.aws/config or ~/.aws/credentials but
// make no remote API call. Provisioning would be a no-op at best and a confusing
// "missing credentials" error at worst when the user is just inspecting state.
needsauth.NotWhenContainsArgs("configure", "list"),
needsauth.NotWhenContainsArgs("configure", "list-profiles"),
needsauth.NotWhenContainsArgs("configure", "get"),
needsauth.NotWhenContainsArgs("configure", "set"),
),
Uses: []schema.CredentialUsage{
// Both credential types are optional so that users can link only the one
// they use: SSO-only users (no IAM access key) can set up the plugin without
// being forced to provide an Access Key, and vice versa. At runtime, each
// provisioner yields silently when the active profile is configured for the
// other credential type.
{
Name: credname.AccessKey,
Provisioner: CLIProvisioner{},
Optional: true,
},
{
Name: credname.SSOProfile,
Provisioner: SSOCLIProvisioner{},
Optional: true,
},
},
}
Expand Down
6 changes: 6 additions & 0 deletions plugins/aws/awslogs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func awslogsCli() schema.Executable {
{
Name: credname.AccessKey,
Provisioner: CLIProvisioner{},
Optional: true,
},
{
Name: credname.SSOProfile,
Provisioner: SSOCLIProvisioner{},
Optional: true,
},
},
}
Expand Down
5 changes: 5 additions & 0 deletions plugins/aws/cache_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
const (
mfaCacheKeyID = "sts-mfa"
assumeRoleCacheKeyID = "sts-assume-role"
ssoRoleCacheKeyID = "sso-role"
)

// stsCacheWriter writes aws temp credentials to cache using the awsCacheKey
Expand All @@ -36,3 +37,7 @@ func getRoleCacheKey(roleArn string, accessKeyID string) string {
func getMfaCacheKey(accessKeyID string) string {
return fmt.Sprintf("%s|%s", mfaCacheKeyID, accessKeyID)
}

func getSSORoleCacheKey(accountID, roleName, sessionKey string) string {
return fmt.Sprintf("%s|%s|%s|%s", ssoRoleCacheKeyID, accountID, roleName, sessionKey)
}
6 changes: 6 additions & 0 deletions plugins/aws/cdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func AWSCDKToolkit() schema.Executable {
{
Name: credname.AccessKey,
Provisioner: CLIProvisioner{},
Optional: true,
},
{
Name: credname.SSOProfile,
Provisioner: SSOCLIProvisioner{},
Optional: true,
},
},
}
Expand Down
26 changes: 26 additions & 0 deletions plugins/aws/cli_provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,29 @@ func (p CLIProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput
func (p CLIProvisioner) Description() string {
return "Provision environment variables with master credentials or temporary STS credentials AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN"
}

// SSOCLIProvisioner provisions SSO role credentials when the AWS CLI is invoked,
// after stripping any --profile flag from the command line so the AWS CLI does
// not try to assume a role on its own.
type SSOCLIProvisioner struct {
}

func (p SSOCLIProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) {
profile, editedCommandLine, err := stripAndReturnProfileFlag(out.CommandLine)
if err != nil {
out.AddError(err)
return
}
if editedCommandLine != nil {
out.CommandLine = editedCommandLine
}
NewSSOProvisioner(profile).Provision(ctx, in, out)
}

func (p SSOCLIProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) {
// Nothing to do here: environment variables get wiped automatically when the process exits.
}

func (p SSOCLIProvisioner) Description() string {
return "Provision environment variables with temporary AWS IAM Identity Center role credentials AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN"
}
6 changes: 6 additions & 0 deletions plugins/aws/eksctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func eksctlCLI() schema.Executable {
{
Name: credname.AccessKey,
Provisioner: CLIProvisioner{},
Optional: true,
},
{
Name: credname.SSOProfile,
Provisioner: SSOCLIProvisioner{},
Optional: true,
},
},
}
Expand Down
1 change: 1 addition & 0 deletions plugins/aws/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func New() schema.Plugin {
},
Credentials: []schema.CredentialType{
AccessKey(),
SSOProfile(),
},
Executables: []schema.Executable{
AWSCLI(),
Expand Down
6 changes: 6 additions & 0 deletions plugins/aws/sam.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ func AWSSAMCLI() schema.Executable {
{
Name: credname.AccessKey,
Provisioner: CLIProvisioner{},
Optional: true,
},
{
Name: credname.SSOProfile,
Provisioner: SSOCLIProvisioner{},
Optional: true,
},
},
}
Expand Down
Loading