diff --git a/app/app.go b/app/app.go index e9b71e1..a35c46d 100644 --- a/app/app.go +++ b/app/app.go @@ -1291,7 +1291,7 @@ func Init(name string, awsCredentials bool, sessionDuration int) (*App, error) { if awsCredentials { cfg, err := config.LoadDefaultConfig(context.Background()) if err != nil { - return nil, err + return nil, auth.FriendlyAWSConfigError(err) } app.Session = cfg app.AWS = apppackaws.New(cfg) diff --git a/auth/auth.go b/auth/auth.go index ce649de..35005b3 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/url" + "strings" "github.com/apppackio/apppack/state" "github.com/aws/aws-sdk-go-v2/aws" @@ -13,6 +14,38 @@ import ( "github.com/sirupsen/logrus" ) +// ignoreSharedConfigFiles disables loading of the local AWS shared config and +// credentials files (~/.aws/config, ~/.aws/credentials). AppPack's OAuth flow +// injects its own credentials, so those files are never needed here. Without +// this, a partially-configured [default] profile on the user's machine makes +// every non-`auth` command fail with an opaque SDK error. +func ignoreSharedConfigFiles() config.LoadOptionsFunc { + return func(o *config.LoadOptions) error { + o.SharedConfigFiles = []string{} + o.SharedCredentialsFiles = []string{} + return nil + } +} + +// FriendlyAWSConfigError adds a hint to errors returned when loading the local +// AWS configuration (the `--aws-credentials` flow, which intentionally reads +// the user's ~/.aws files). The raw SDK "partial credentials" error is opaque; +// this points the user at the real, machine-local cause. +func FriendlyAWSConfigError(err error) error { + if err == nil { + return nil + } + if strings.Contains(err.Error(), "partial credentials") { + return fmt.Errorf( + "%w: your local AWS credentials appear to be incomplete — "+ + "check for a partial [default] profile in ~/.aws/credentials or ~/.aws/config, "+ + "or stray AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY environment variables", + err, + ) + } + return err +} + const ( deviceCodeURL = "https://auth.apppack.io/oauth/device/code" oauthTokenURL = "https://auth.apppack.io/oauth/token" // #nosec G101 -- URL path, not a credential @@ -53,6 +86,7 @@ func AppAWSSession(appName string, sessionDuration int) (aws.Config, *AppRole, e *creds.SessionToken, )), config.WithRegion(appRole.Region), + ignoreSharedConfigFiles(), ) if err != nil { return aws.Config{}, nil, err @@ -90,6 +124,7 @@ func AdminAWSSession(idOrAlias string, sessionDuration int, region string) (aws. *creds.SessionToken, )), config.WithRegion(region), + ignoreSharedConfigFiles(), ) if err != nil { return aws.Config{}, nil, err diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 0000000..3819990 --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,37 @@ +package auth + +import ( + "errors" + "strings" + "testing" +) + +func TestFriendlyAWSConfigError(t *testing.T) { + t.Run("nil error stays nil", func(t *testing.T) { + if err := FriendlyAWSConfigError(nil); err != nil { + t.Fatalf("expected nil, got %v", err) + } + }) + + t.Run("partial credentials error gets a hint", func(t *testing.T) { + raw := errors.New("error fetching config from profile, default, Error using profile: \n 2, partial credentials found for profile default") + got := FriendlyAWSConfigError(raw) + if got == nil { + t.Fatal("expected wrapped error, got nil") + } + if !strings.Contains(got.Error(), "local AWS credentials appear to be incomplete") { + t.Errorf("expected friendly hint, got: %v", got) + } + if !errors.Is(got, raw) { + t.Error("expected wrapped error to preserve the original via errors.Is") + } + }) + + t.Run("unrelated error is passed through unchanged", func(t *testing.T) { + raw := errors.New("some other failure") + got := FriendlyAWSConfigError(raw) + if !errors.Is(got, raw) || got.Error() != raw.Error() { + t.Errorf("expected passthrough, got: %v", got) + } + }) +} diff --git a/auth/tokens.go b/auth/tokens.go index e25b316..8f79c76 100644 --- a/auth/tokens.go +++ b/auth/tokens.go @@ -233,6 +233,7 @@ func (t *Tokens) GetCredentials(role Role, duration int) (*types.Credentials, er cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion("us-east-1"), // STS is a global service, use us-east-1 as default + ignoreSharedConfigFiles(), ) if err != nil { return nil, err diff --git a/cmd/access.go b/cmd/access.go index 1051e6a..8f4d087 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -86,12 +86,13 @@ func adminSession(sessionDuration int) (aws.Config, error) { if UseAWSCredentials { ctx := context.Background() if region != "" { - return config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + return cfg, auth.FriendlyAWSConfigError(err) } cfg, err := config.LoadDefaultConfig(ctx) if err != nil { - return aws.Config{}, err + return aws.Config{}, auth.FriendlyAWSConfigError(err) } if cfg.Region == "" {