package rootaws import ( "bytes" "context" "log" "os" "path/filepath" "strings" "testing" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" ) func TestParseStaticCredentials(t *testing.T) { valid := "# comment\n[default]\naws_access_key_id = ACCESS\naws_secret_access_key = SECRET/+==\naws_session_token = TOKEN/+==\n" got, err := ParseStaticCredentials([]byte(valid)) if err != nil || got.AccessKeyID != "ACCESS" || got.SecretAccessKey != "SECRET/+==" || got.SessionToken != "TOKEN/+==" { t.Fatalf("static parse failed: %v", err) } for name, body := range map[string]string{ "process": valid + "credential_process = touch /never-run\n", "unknown": valid + "region = eu-west-1\n", "other-profile": valid + "[other]\ncredential_process = bad\n", "duplicate-profile": valid + "[default]\n", "duplicate-key": valid + "aws_access_key_id = SECOND\n", "missing-secret": "[default]\naws_access_key_id=ACCESS\n", "no-profile": "aws_access_key_id=ACCESS\naws_secret_access_key=SECRET\n", "empty": "", "nul": valid + "\x00", "oversize": strings.Repeat("#", MaxCredentialsSize+1), } { t.Run(name, func(t *testing.T) { if _, err := ParseStaticCredentials([]byte(body)); err == nil { t.Fatal("accepted invalid static credentials") } }) } } func TestParseStaticCredentialsSDKSemantics(t *testing.T) { parse := func(access, secret string) (string, string, error) { got, err := ParseStaticCredentials([]byte("[default]\naws_access_key_id = " + access + "\naws_secret_access_key = " + secret + "\n")) return got.AccessKeyID, got.SecretAccessKey, err } for name, tc := range map[string]struct{ access, secret, wantAccess, wantSecret string }{ "embedded-hash": {"ACC#ESS", "SEC#RET", "ACC#ESS", "SEC#RET"}, "embedded-semicolon": {"ACC;ESS", "SEC;RET", "ACC;ESS", "SEC;RET"}, "space-hash-comment": {"ACCESS # note", "SECRET ; note", "ACCESS", "SECRET"}, "tab-comment": {"ACCESS\t# note", "SECRET\t; note", "ACCESS", "SECRET"}, "double-quotes": {`"ACCESS"`, `"SEC#RET"`, "ACCESS", "SEC#RET"}, "single-quotes": {`'ACCESS'`, `'SEC;RET'`, "ACCESS", "SEC;RET"}, "quotes-and-comments": {`"ACCESS" # note`, `'SECRET' ; note`, "ACCESS", "SECRET"}, "one-pair-only": {`""ACCESS""`, `'"SECRET"'`, `"ACCESS"`, `"SECRET"`}, "mismatched-quotes": {`"ACCESS'`, `'SECRET`, `"ACCESS'`, `'SECRET`}, "lone-quote": {`"`, `'`, `"`, `'`}, } { t.Run(name, func(t *testing.T) { access, secret, err := parse(tc.access, tc.secret) if err != nil || access != tc.wantAccess || secret != tc.wantSecret { t.Fatalf("parsed %q %q, %v", access, secret, err) } }) } for name, body := range map[string]string{ "empty-double-quotes": "[default]\naws_access_key_id = \"\"\naws_secret_access_key = SECRET\n", "empty-single-quotes": "[default]\naws_access_key_id = ACCESS\naws_secret_access_key = ''\n", "comment-only-value": "[default]\naws_access_key_id = # note\naws_secret_access_key = SECRET\n", "quoted-spaces": "[default]\naws_access_key_id = \"AC CESS\"\naws_secret_access_key = SECRET\n", "process-with-quotes": "[default]\naws_access_key_id = ACCESS\naws_secret_access_key = SECRET\n\"credential_process\" = bad\n", } { t.Run(name, func(t *testing.T) { if _, err := ParseStaticCredentials([]byte(body)); err == nil { t.Fatal("accepted invalid static credentials") } }) } } func TestRootConfigEnvironmentKeysIgnoreMalformedFile(t *testing.T) { for _, token := range []string{"", "ENV_TOKEN"} { t.Run("token="+token, func(t *testing.T) { isolatedAWSEnv(t) t.Setenv("AWS_ACCESS_KEY_ID", "ENV_ACCESS") t.Setenv("AWS_SECRET_ACCESS_KEY", "ENV_SECRET") t.Setenv("AWS_SESSION_TOKEN", token) path := filepath.Join(t.TempDir(), "credentials") if err := os.WriteFile(path, []byte("[default]\ncredential_process = touch /never-run\n[other]\n"), 0600); err != nil { t.Fatal(err) } cfg, err := loadRootConfig(context.Background(), path) if err != nil { t.Fatalf("malformed unused file made config fatal: %v", err) } got, err := cfg.Credentials.Retrieve(context.Background()) if err != nil || got.AccessKeyID != "ENV_ACCESS" || got.SecretAccessKey != "ENV_SECRET" || got.SessionToken != token { t.Fatalf("environment credentials failed: %v", err) } }) } } func TestRootConfigIncompleteEnvironmentKeysStillValidateFile(t *testing.T) { isolatedAWSEnv(t) t.Setenv("AWS_ACCESS_KEY_ID", "ENV_ACCESS") path := filepath.Join(t.TempDir(), "credentials") if err := os.WriteFile(path, []byte("[default]\ncredential_process = bad\n"), 0600); err != nil { t.Fatal(err) } cfg, err := loadRootConfig(context.Background(), path) if err != nil { t.Fatalf("unused unsafe credentials made config loading fatal: %v", err) } if _, err := cfg.Credentials.Retrieve(context.Background()); err == nil { t.Fatal("malformed file accepted without a complete environment key pair") } } func TestRootConfigEnvironmentKeysBeatCallerCredentials(t *testing.T) { isolatedAWSEnv(t) t.Setenv("AWS_ACCESS_KEY_ID", "ENV_ACCESS") t.Setenv("AWS_SECRET_ACCESS_KEY", "ENV_SECRET") caller := awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("CALLER", "CALLER_SECRET", "")) cfg, err := loadRootConfig(context.Background(), filepath.Join(t.TempDir(), "absent"), caller) if err != nil { t.Fatal(err) } got, err := cfg.Credentials.Retrieve(context.Background()) if err != nil || got.AccessKeyID != "ENV_ACCESS" || got.SecretAccessKey != "ENV_SECRET" { t.Fatalf("caller credentials overrode the environment: %v", err) } } func isolatedAWSEnv(t *testing.T) { t.Helper() for _, key := range []string{"AWS_PROFILE", "AWS_DEFAULT_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY", "AWS_SECRET_KEY", "AWS_SESSION_TOKEN", "AWS_WEB_IDENTITY_TOKEN_FILE", "AWS_ROLE_ARN", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "AWS_CONTAINER_CREDENTIALS_FULL_URI"} { t.Setenv(key, "") } t.Setenv("AWS_EC2_METADATA_DISABLED", "true") t.Setenv("AWS_REGION", "eu-west-1") } func TestRootConfigNeverLoadsSharedFiles(t *testing.T) { for _, selector := range []string{"HOME", "AWS_CONFIG_FILE", "AWS_SHARED_CREDENTIALS_FILE", "options", "profile"} { t.Run(selector, func(t *testing.T) { isolatedAWSEnv(t) dir := t.TempDir() t.Setenv("HOME", dir) t.Setenv("AWS_CONFIG_FILE", filepath.Join(dir, "missing-config")) t.Setenv("AWS_SHARED_CREDENTIALS_FILE", filepath.Join(dir, "missing-keys")) marker := filepath.Join(dir, "executed") // If the SDK ever selects this profile, retrieval executes touch. body := "[default]\ncredential_process = touch " + marker + "\n" bad := filepath.Join(dir, "untrusted") if err := os.WriteFile(bad, []byte(body), 0600); err != nil { t.Fatal(err) } var opts []func(*awsconfig.LoadOptions) error switch selector { case "HOME": if err := os.Mkdir(filepath.Join(dir, ".aws"), 0700); err != nil { t.Fatal(err) } for _, name := range []string{"config", "credentials"} { if err := os.WriteFile(filepath.Join(dir, ".aws", name), []byte(body), 0600); err != nil { t.Fatal(err) } } t.Setenv("AWS_CONFIG_FILE", "") t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "") case "options": opts = append(opts, awsconfig.WithSharedConfigFiles([]string{bad}), awsconfig.WithSharedCredentialsFiles([]string{bad})) case "profile": t.Setenv("AWS_PROFILE", "default") t.Setenv("AWS_CONFIG_FILE", bad) default: t.Setenv(selector, bad) } cfg, err := loadRootConfig(context.Background(), filepath.Join(dir, "absent"), opts...) if err == nil { if _, err := cfg.Credentials.Retrieve(context.Background()); err == nil { t.Fatal("unexpected credentials without an allowed source") } } else if selector != "profile" { t.Fatal(err) } if _, err := os.Stat(marker); !os.IsNotExist(err) { t.Fatal("credential_process executed") } }) } } func TestRootConfigEnvironmentCredentials(t *testing.T) { isolatedAWSEnv(t) t.Setenv("AWS_ACCESS_KEY_ID", "ENV_ACCESS") t.Setenv("AWS_SECRET_ACCESS_KEY", "ENV_SECRET") t.Setenv("AWS_SESSION_TOKEN", "ENV_TOKEN") cfg, err := loadRootConfig(context.Background(), filepath.Join(t.TempDir(), "absent")) if err != nil { t.Fatal(err) } got, err := cfg.Credentials.Retrieve(context.Background()) if err != nil || got.AccessKeyID != "ENV_ACCESS" || got.SecretAccessKey != "ENV_SECRET" || got.SessionToken != "ENV_TOKEN" { t.Fatalf("environment credentials failed: %v", err) } } func TestRootConfigRefusesProfilesExplicitly(t *testing.T) { for _, name := range []string{"AWS_PROFILE", "AWS_DEFAULT_PROFILE"} { t.Run(name, func(t *testing.T) { isolatedAWSEnv(t) t.Setenv("AWS_ACCESS_KEY_ID", "ENV_ACCESS") t.Setenv("AWS_SECRET_ACCESS_KEY", "ENV_SECRET") t.Setenv(name, "default") var logs bytes.Buffer old := log.Writer() log.SetOutput(&logs) defer log.SetOutput(old) _, err := loadRootConfig(context.Background(), filepath.Join(t.TempDir(), "absent")) if err == nil || !strings.Contains(err.Error(), "do not support") || !strings.Contains(logs.String(), "ALERT:") { t.Fatalf("profile not explicitly refused with an alert: %v", err) } }) } } func TestNonRootConfigKeepsSharedCredentials(t *testing.T) { if os.Geteuid() == 0 { t.Skip("requires non-root to exercise unchanged SDK behavior") } isolatedAWSEnv(t) dir := t.TempDir() path := filepath.Join(dir, "credentials") if err := os.WriteFile(path, []byte("[default]\naws_access_key_id=SHARED\naws_secret_access_key=SECRET\n"), 0600); err != nil { t.Fatal(err) } t.Setenv("AWS_SHARED_CREDENTIALS_FILE", path) t.Setenv("AWS_CONFIG_FILE", filepath.Join(dir, "absent")) cfg, err := LoadDefaultConfig(context.Background()) if err != nil { t.Fatal(err) } got, err := cfg.Credentials.Retrieve(context.Background()) if err != nil || got.AccessKeyID != "SHARED" { t.Fatalf("non-root shared credentials changed: %v", err) } } func TestRootConfigUnsafeFileRefusesOnUseWithoutFallback(t *testing.T) { isolatedAWSEnv(t) path := filepath.Join(t.TempDir(), "credentials") if err := os.WriteFile(path, []byte("[default]\ncredential_process=bad\n"), 0600); err != nil { t.Fatal(err) } fallback := credentials.NewStaticCredentialsProvider("OTHER_IDENTITY", "SECRET", "") cfg, err := loadRootConfig(context.Background(), path, awsconfig.WithCredentialsProvider(fallback)) if err != nil { t.Fatalf("opening an unused store would abort adoption: %v", err) } if got, err := cfg.Credentials.Retrieve(context.Background()); err == nil || got.AccessKeyID != "" { t.Fatal("unsafe file fell through to another identity") } } func TestRootCredentialsOffcloudSelection(t *testing.T) { isolatedAWSEnv(t) path := filepath.Join(t.TempDir(), "credentials") got, err := offcloudCredentials(path) if err != nil || got.HasKeys() { t.Fatalf("absent file: credentials=%v err=%v", got.HasKeys(), err) } if err := os.WriteFile(path, []byte("[default]\ncredential_process=bad\n"), 0600); err != nil { t.Fatal(err) } if _, err := offcloudCredentials(path); err == nil { t.Fatal("unsafe file was accepted") } t.Run("incomplete environment keys", func(t *testing.T) { t.Setenv("AWS_ACCESS_KEY_ID", "ENV_ACCESS") if _, err := offcloudCredentials(path); err == nil { t.Fatal("incomplete environment keys bypassed the unsafe file") } }) for _, names := range [][2]string{{"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"}, {"AWS_ACCESS_KEY", "AWS_SECRET_KEY"}} { t.Run(names[0], func(t *testing.T) { t.Setenv(names[0], "ENV_ACCESS") t.Setenv(names[1], "ENV_SECRET") t.Setenv("AWS_SESSION_TOKEN", "ENV_TOKEN") got, err := offcloudCredentials(path) if err != nil || got.HasKeys() || got.SessionToken != "" { t.Fatalf("environment keys must bypass file and leave static fields unset: %v", err) } }) } }