From 3a37815694f078c018bd612c0a5c9d361b551032 Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Sun, 8 Mar 2026 12:19:31 -0500 Subject: [PATCH 1/9] feat: Suggest profiles when no auth config and clean up legacy keyring entries - Add profile discovery hint to `atmos auth list` when no providers/identities configured - Use error builder pattern with ErrAuthNotConfigured sentinel for rich error messages - Suggest using --profile flag to load auth from profiles (e.g. profiles/developers/) - Clean up legacy (pre-realm) keyring entries after successful credential caching - Prevent realm mismatch warnings by removing outdated keyring entries - Add comprehensive tests for profile suggestion logic and edge cases Co-Authored-By: Claude Haiku 4.5 --- cmd/auth_list.go | 47 +++++++++++++-- cmd/auth_list_test.go | 79 ++++++++++++++++++++++++ pkg/auth/manager_chain.go | 4 ++ pkg/auth/manager_whoami.go | 2 + pkg/auth/realm_mismatch.go | 14 +++++ pkg/auth/realm_mismatch_test.go | 104 ++++++++++++++++++++++++++++++++ 6 files changed, 244 insertions(+), 6 deletions(-) diff --git a/cmd/auth_list.go b/cmd/auth_list.go index 96c0950100..bc8edde86f 100644 --- a/cmd/auth_list.go +++ b/cmd/auth_list.go @@ -16,6 +16,7 @@ import ( cfg "github.com/cloudposse/atmos/pkg/config" log "github.com/cloudposse/atmos/pkg/logger" "github.com/cloudposse/atmos/pkg/perf" + "github.com/cloudposse/atmos/pkg/profile" "github.com/cloudposse/atmos/pkg/schema" ) @@ -136,7 +137,7 @@ func executeAuthListCommand(cmd *cobra.Command, args []string) error { } // Load auth manager. - authManager, err := loadAuthManagerForList() + authManager, atmosConfig, err := loadAuthManagerForList() if err != nil { return err } @@ -151,6 +152,14 @@ func executeAuthListCommand(cmd *cobra.Command, args []string) error { return err } + // When no providers or identities are configured, check if profiles exist + // that might contain auth configuration and suggest using --profile. + if len(filteredProviders) == 0 && len(filteredIdentities) == 0 { + if suggestion := suggestProfilesForAuth(atmosConfig); suggestion != nil { + return suggestion + } + } + // Get output format. format, _ := cmd.Flags().GetString("format") @@ -364,19 +373,45 @@ func renderYAML(providers map[string]schema.Provider, identities map[string]sche return string(data), nil } -// loadAuthManager loads the auth manager (helper from auth_whoami.go). -func loadAuthManagerForList() (authTypes.AuthManager, error) { +// suggestProfilesForAuth checks if profiles exist and returns a helpful error +// suggesting the user try --profile when no auth providers/identities are configured. +func suggestProfilesForAuth(atmosConfig *schema.AtmosConfiguration) error { + defer perf.Track(atmosConfig, "cmd.suggestProfilesForAuth")() + + mgr := profile.NewProfileManager() + profiles, err := mgr.ListProfiles(atmosConfig) + if err != nil || len(profiles) == 0 { + return nil + } + + // Collect profile names. + profileNames := make([]string, 0, len(profiles)) + for _, p := range profiles { + profileNames = append(profileNames, p.Name) + } + + return errUtils.Build(errUtils.ErrAuthNotConfigured). + WithExplanation("No authentication providers or identities are configured in the current configuration"). + WithHintf("Available profiles: `%s`", strings.Join(profileNames, "`, `")). + WithHint("Try: `atmos auth list --profile ` to load auth configuration from a profile"). + WithHint("Run `atmos profile list` for detailed information about each profile"). + WithExitCode(1). + Err() +} + +// loadAuthManagerForList loads the auth manager and returns the atmos config for profile discovery. +func loadAuthManagerForList() (authTypes.AuthManager, *schema.AtmosConfiguration, error) { defer perf.Track(nil, "cmd.loadAuthManagerForList")() atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false) if err != nil { - return nil, fmt.Errorf("%w: failed to load atmos config: %w", errUtils.ErrInvalidAuthConfig, err) + return nil, nil, fmt.Errorf("%w: failed to load atmos config: %w", errUtils.ErrInvalidAuthConfig, err) } manager, err := createAuthManager(&atmosConfig.Auth, atmosConfig.CliConfigPath) if err != nil { - return nil, fmt.Errorf("%w: failed to create auth manager: %w", errUtils.ErrInvalidAuthConfig, err) + return nil, nil, fmt.Errorf("%w: failed to create auth manager: %w", errUtils.ErrInvalidAuthConfig, err) } - return manager, nil + return manager, &atmosConfig, nil } diff --git a/cmd/auth_list_test.go b/cmd/auth_list_test.go index f45a8803e2..610fd7e3a7 100644 --- a/cmd/auth_list_test.go +++ b/cmd/auth_list_test.go @@ -2,12 +2,16 @@ package cmd import ( "os" + "path/filepath" + "strings" "testing" + cockroachErrors "github.com/cockroachdb/errors" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + errUtils "github.com/cloudposse/atmos/errors" "github.com/cloudposse/atmos/pkg/schema" ) @@ -430,6 +434,81 @@ func TestProvidersFlagCompletion_ReturnsSortedProviders(t *testing.T) { assert.Equal(t, []string{"apple-provider", "mango-provider", "zebra-provider"}, results) } +func TestExecuteAuthListCommand_SuggestsProfilesWhenNoAuthConfigured(t *testing.T) { + // Setup: Create a temp dir with minimal atmos.yaml (no auth providers/identities) + // but with profiles that could contain auth config. + tempDir := t.TempDir() + configFile := filepath.Join(tempDir, "atmos.yaml") + + configContent := `auth: + realm: test + logs: + level: Debug +` + err := os.WriteFile(configFile, []byte(configContent), 0o600) + require.NoError(t, err) + + // Create profile directories (simulating profiles that may contain auth config). + for _, profileName := range []string{"developers", "devops", "superadmin"} { + profileDir := filepath.Join(tempDir, "profiles", profileName) + err := os.MkdirAll(profileDir, 0o755) + require.NoError(t, err) + + // Add a minimal atmos.yaml in each profile so it's discovered. + profileConfig := filepath.Join(profileDir, "atmos.yaml") + err = os.WriteFile(profileConfig, []byte("# profile config\n"), 0o600) + require.NoError(t, err) + } + + // Set environment to use test config. + t.Chdir(tempDir) + + _ = NewTestKit(t) + + cmd := createTestAuthListCmd() + cmd.RunE = executeAuthListCommand + + // Execute the command - should return an error suggesting profiles. + err = cmd.Execute() + + require.Error(t, err) + assert.ErrorIs(t, err, errUtils.ErrAuthNotConfigured) + + // Verify hints contain profile names and usage suggestion. + hints := cockroachErrors.GetAllHints(err) + allHints := strings.Join(hints, " ") + assert.Contains(t, allHints, "developers") + assert.Contains(t, allHints, "devops") + assert.Contains(t, allHints, "superadmin") + assert.Contains(t, allHints, "--profile") +} + +func TestExecuteAuthListCommand_NoErrorWhenNoProfilesExist(t *testing.T) { + // Setup: Create a temp dir with minimal atmos.yaml (no auth, no profiles). + tempDir := t.TempDir() + configFile := filepath.Join(tempDir, "atmos.yaml") + + configContent := `auth: + realm: test +` + err := os.WriteFile(configFile, []byte(configContent), 0o600) + require.NoError(t, err) + + // No profiles directory created. + + t.Chdir(tempDir) + + _ = NewTestKit(t) + + cmd := createTestAuthListCmd() + cmd.RunE = executeAuthListCommand + + // Execute the command - should NOT return an error (no profiles to suggest). + err = cmd.Execute() + + assert.NoError(t, err) +} + func TestIdentitiesFlagCompletion_ReturnsSortedIdentities(t *testing.T) { // Setup: Create a test config file with identities in non-alphabetical order. tempDir := t.TempDir() diff --git a/pkg/auth/manager_chain.go b/pkg/auth/manager_chain.go index 2422a9df74..67ee106ba8 100644 --- a/pkg/auth/manager_chain.go +++ b/pkg/auth/manager_chain.go @@ -328,6 +328,8 @@ func (m *manager) authenticateWithProvider(ctx context.Context, providerName str log.Debug("Failed to cache provider credentials", "error", err) } else { log.Debug("Cached provider credentials", "providerName", providerName) + // Clean up legacy (pre-realm) keyring entry to prevent realm mismatch warnings. + m.deleteLegacyKeyringEntry(providerName) } } @@ -504,6 +506,8 @@ func (m *manager) authenticateIdentityChain(ctx context.Context, startIndex int, log.Debug("Failed to cache credentials", "identityStep", identityStep, "error", err) } else { log.Debug("Cached credentials", "identityStep", identityStep) + // Clean up legacy (pre-realm) keyring entry to prevent realm mismatch warnings. + m.deleteLegacyKeyringEntry(identityStep) } } diff --git a/pkg/auth/manager_whoami.go b/pkg/auth/manager_whoami.go index e7fd7e4540..ff98ba99c3 100644 --- a/pkg/auth/manager_whoami.go +++ b/pkg/auth/manager_whoami.go @@ -46,6 +46,8 @@ func (m *manager) buildWhoamiInfo(identityName string, creds types.ICredentials) // Note: We keep info.Credentials populated for validation purposes. // The Credentials field is marked with json:"-" yaml:"-" tags to prevent // accidental serialization, so there's no security risk in keeping it. + // Clean up legacy (pre-realm) keyring entry to prevent realm mismatch warnings. + m.deleteLegacyKeyringEntry(identityName) } } else { log.Debug("Skipping keyring cache for session tokens in WhoamiInfo", logKeyIdentity, identityName) diff --git a/pkg/auth/realm_mismatch.go b/pkg/auth/realm_mismatch.go index 6c9d786604..2e95ac8057 100644 --- a/pkg/auth/realm_mismatch.go +++ b/pkg/auth/realm_mismatch.go @@ -120,6 +120,20 @@ func hasCredentialFiles(awsDir string) bool { return false } +// deleteLegacyKeyringEntry removes a pre-realm keyring entry after credentials +// have been successfully stored under the current realm. This prevents the +// realm mismatch warning from firing on subsequent commands. +func (m *manager) deleteLegacyKeyringEntry(alias string) { + if m.realm.Value == "" || m.credentialStore == nil { + return + } + if err := m.credentialStore.Delete(alias, ""); err != nil { + log.Debug("No legacy keyring entry to delete", "alias", alias) + } else { + log.Debug("Deleted legacy keyring entry (pre-realm)", "alias", alias) + } +} + // logRealmMismatchWarning emits a warning about credentials existing under a different realm. func logRealmMismatchWarning(currentRealm, alternateRealm string) { currentDisplay := currentRealm diff --git a/pkg/auth/realm_mismatch_test.go b/pkg/auth/realm_mismatch_test.go index 19434356ce..511866013b 100644 --- a/pkg/auth/realm_mismatch_test.go +++ b/pkg/auth/realm_mismatch_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/cloudposse/atmos/pkg/auth/realm" + "github.com/cloudposse/atmos/pkg/auth/types" ) func TestCheckRealmMismatchFiles_NonEmptyRealm_FindsNoRealmCreds(t *testing.T) { @@ -291,6 +292,62 @@ func TestCheckNoRealmCredentials(t *testing.T) { }) } +func TestDeleteLegacyKeyringEntry_CleansUpEmptyRealm(t *testing.T) { + // Store has credentials under both realms. + store := &realmAwareTestStore{ + data: map[string]map[string]types.ICredentials{ + "my-identity": { + "": &testCreds{}, + "my-realm": &testCreds{}, + }, + }, + } + m := &manager{ + credentialStore: store, + realm: realm.RealmInfo{Value: "my-realm", Source: "config"}, + } + + m.deleteLegacyKeyringEntry("my-identity") + + // Legacy (empty-realm) entry should be deleted. + _, err := store.Retrieve("my-identity", "") + assert.Error(t, err, "legacy entry should have been deleted") + + // Current realm entry should be preserved. + _, err = store.Retrieve("my-identity", "my-realm") + assert.NoError(t, err, "current realm entry should remain") +} + +func TestDeleteLegacyKeyringEntry_NoOpForEmptyRealm(t *testing.T) { + // When realm is empty, no cleanup should happen. + store := &realmAwareTestStore{ + data: map[string]map[string]types.ICredentials{ + "my-identity": { + "": &testCreds{}, + }, + }, + } + m := &manager{ + credentialStore: store, + realm: realm.RealmInfo{Value: "", Source: "auto"}, + } + + m.deleteLegacyKeyringEntry("my-identity") + + // Entry should still exist — no cleanup for empty realm. + _, err := store.Retrieve("my-identity", "") + assert.NoError(t, err, "entry should not be deleted when realm is empty") +} + +func TestDeleteLegacyKeyringEntry_NilStore(t *testing.T) { + m := &manager{ + credentialStore: nil, + realm: realm.RealmInfo{Value: "my-realm", Source: "config"}, + } + // Should not panic. + m.deleteLegacyKeyringEntry("my-identity") +} + func TestLogRealmMismatchWarning_NonEmptyToEmpty(t *testing.T) { // Verify it doesn't panic. The actual log output goes to the logger. logRealmMismatchWarning("my-realm", "(no realm)") @@ -300,3 +357,50 @@ func TestLogRealmMismatchWarning_EmptyToNonEmpty(t *testing.T) { // Verify it doesn't panic. logRealmMismatchWarning("", "my-project") } + +// realmAwareTestStore is a test credential store that tracks realm for each entry. +// Unlike testStore, this distinguishes between credentials stored under different realms. +type realmAwareTestStore struct { + data map[string]map[string]types.ICredentials // alias -> realm -> creds. +} + +func (s *realmAwareTestStore) Store(alias string, creds types.ICredentials, realmValue string) error { + if s.data == nil { + s.data = map[string]map[string]types.ICredentials{} + } + if s.data[alias] == nil { + s.data[alias] = map[string]types.ICredentials{} + } + s.data[alias][realmValue] = creds + return nil +} + +func (s *realmAwareTestStore) Retrieve(alias string, realmValue string) (types.ICredentials, error) { + if s.data == nil { + return nil, assert.AnError + } + realms, ok := s.data[alias] + if !ok { + return nil, assert.AnError + } + creds, ok := realms[realmValue] + if !ok { + return nil, assert.AnError + } + return creds, nil +} + +func (s *realmAwareTestStore) Delete(alias string, realmValue string) error { + if s.data != nil { + if realms, ok := s.data[alias]; ok { + delete(realms, realmValue) + } + } + return nil +} + +func (s *realmAwareTestStore) List(realmValue string) ([]string, error) { return nil, nil } +func (s *realmAwareTestStore) IsExpired(alias string, realmValue string) (bool, error) { + return false, nil +} +func (s *realmAwareTestStore) Type() string { return "realm-aware-test" } From 84295f6ad2b806ad6eedcb32c713234c2ab553cd Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Sun, 8 Mar 2026 13:21:01 -0500 Subject: [PATCH 2/9] fix: Clean up legacy pre-realm credentials during auth login After switching to realm-scoped credentials (auth.realm), old credentials stored without a realm triggered a persistent warning on every command. The cleanup now runs in Authenticate() so it executes even when cached credentials allow the provider auth step to be skipped. Removes both legacy keyring entries and file-based credentials at the no-realm path. Co-Authored-By: Claude Opus 4.6 --- pkg/auth/manager.go | 8 +++++++ pkg/auth/manager_chain.go | 4 ---- pkg/auth/realm_mismatch.go | 39 ++++++++++++++++++++++++++++++ pkg/auth/realm_mismatch_test.go | 42 +++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/pkg/auth/manager.go b/pkg/auth/manager.go index 6ffec43f83..8587cdd6bd 100644 --- a/pkg/auth/manager.go +++ b/pkg/auth/manager.go @@ -231,6 +231,14 @@ func (m *manager) Authenticate(ctx context.Context, identityName string) (*types m.chain = chain log.Debug("Authentication chain discovered", logKeyIdentity, identityName, "chainLength", len(chain), "chain", chain) + // Clean up legacy (pre-realm) keyring and file entries to prevent realm mismatch warnings. + // This runs early in the Authenticate flow so it executes even when cached credentials + // allow the provider authentication step to be skipped. + m.deleteLegacyCredentialFiles() + for _, step := range chain { + m.deleteLegacyKeyringEntry(step) + } + // Perform credential chain authentication (bottom-up). finalCreds, err := m.authenticateChain(ctx, identityName) if err != nil { diff --git a/pkg/auth/manager_chain.go b/pkg/auth/manager_chain.go index 67ee106ba8..2422a9df74 100644 --- a/pkg/auth/manager_chain.go +++ b/pkg/auth/manager_chain.go @@ -328,8 +328,6 @@ func (m *manager) authenticateWithProvider(ctx context.Context, providerName str log.Debug("Failed to cache provider credentials", "error", err) } else { log.Debug("Cached provider credentials", "providerName", providerName) - // Clean up legacy (pre-realm) keyring entry to prevent realm mismatch warnings. - m.deleteLegacyKeyringEntry(providerName) } } @@ -506,8 +504,6 @@ func (m *manager) authenticateIdentityChain(ctx context.Context, startIndex int, log.Debug("Failed to cache credentials", "identityStep", identityStep, "error", err) } else { log.Debug("Cached credentials", "identityStep", identityStep) - // Clean up legacy (pre-realm) keyring entry to prevent realm mismatch warnings. - m.deleteLegacyKeyringEntry(identityStep) } } diff --git a/pkg/auth/realm_mismatch.go b/pkg/auth/realm_mismatch.go index 2e95ac8057..56a706dca4 100644 --- a/pkg/auth/realm_mismatch.go +++ b/pkg/auth/realm_mismatch.go @@ -134,6 +134,45 @@ func (m *manager) deleteLegacyKeyringEntry(alias string) { } } +// deleteLegacyCredentialFiles removes pre-realm credential files after credentials +// have been successfully stored under the current realm. This prevents the file-based +// realm mismatch warning from firing on subsequent commands. +// Scans {baseDir}/aws/{provider}/ for credential and config files and removes them. +func (m *manager) deleteLegacyCredentialFiles() { + if m.realm.Value == "" { + return + } + + baseDir := xdg.LookupXDGConfigDir("") + if baseDir == "" { + return + } + + awsDir := filepath.Join(baseDir, awsDirNameForMismatch) + providerDirs, err := os.ReadDir(awsDir) + if err != nil { + return + } + + for _, provider := range providerDirs { + if !provider.IsDir() { + continue + } + providerDir := filepath.Join(awsDir, provider.Name()) + // Remove credential, config, and lock files from legacy (no-realm) path. + for _, filename := range []string{"credentials", "config", "credentials.lock", "config.lock"} { + filePath := filepath.Join(providerDir, filename) + if err := os.Remove(filePath); err == nil { + log.Debug("Deleted legacy credential file (pre-realm)", "path", filePath) + } + } + // Remove provider directory if now empty. + _ = os.Remove(providerDir) + } + // Remove aws directory if now empty. + _ = os.Remove(awsDir) +} + // logRealmMismatchWarning emits a warning about credentials existing under a different realm. func logRealmMismatchWarning(currentRealm, alternateRealm string) { currentDisplay := currentRealm diff --git a/pkg/auth/realm_mismatch_test.go b/pkg/auth/realm_mismatch_test.go index 511866013b..5086482c81 100644 --- a/pkg/auth/realm_mismatch_test.go +++ b/pkg/auth/realm_mismatch_test.go @@ -348,6 +348,48 @@ func TestDeleteLegacyKeyringEntry_NilStore(t *testing.T) { m.deleteLegacyKeyringEntry("my-identity") } +func TestDeleteLegacyCredentialFiles_CleansUpNoRealmFiles(t *testing.T) { + // Setup: credential files at {baseDir}/aws/{provider}/credentials (no realm). + parent := t.TempDir() + atmosDir := filepath.Join(parent, "atmos") + providerDir := filepath.Join(atmosDir, "aws", "my-provider") + require.NoError(t, os.MkdirAll(providerDir, 0o700)) + require.NoError(t, os.WriteFile(filepath.Join(providerDir, "credentials"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(providerDir, "config"), []byte("test"), 0o600)) + t.Setenv("ATMOS_XDG_CONFIG_HOME", parent) + + m := &manager{ + realm: realm.RealmInfo{Value: "my-realm", Source: "config"}, + } + + m.deleteLegacyCredentialFiles() + + // Legacy files should be deleted. + _, err := os.Stat(filepath.Join(providerDir, "credentials")) + assert.True(t, os.IsNotExist(err), "legacy credentials file should be deleted") + _, err = os.Stat(filepath.Join(providerDir, "config")) + assert.True(t, os.IsNotExist(err), "legacy config file should be deleted") +} + +func TestDeleteLegacyCredentialFiles_NoOpForEmptyRealm(t *testing.T) { + parent := t.TempDir() + atmosDir := filepath.Join(parent, "atmos") + providerDir := filepath.Join(atmosDir, "aws", "my-provider") + require.NoError(t, os.MkdirAll(providerDir, 0o700)) + require.NoError(t, os.WriteFile(filepath.Join(providerDir, "credentials"), []byte("test"), 0o600)) + t.Setenv("ATMOS_XDG_CONFIG_HOME", parent) + + m := &manager{ + realm: realm.RealmInfo{Value: "", Source: "auto"}, + } + + m.deleteLegacyCredentialFiles() + + // Files should NOT be deleted when realm is empty. + _, err := os.Stat(filepath.Join(providerDir, "credentials")) + assert.NoError(t, err, "files should not be deleted when realm is empty") +} + func TestLogRealmMismatchWarning_NonEmptyToEmpty(t *testing.T) { // Verify it doesn't panic. The actual log output goes to the logger. logRealmMismatchWarning("my-realm", "(no realm)") From c2c5a3b3b432f077d49efd88005651bc087352df Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Sun, 8 Mar 2026 13:43:31 -0500 Subject: [PATCH 3/9] fix: Address CodeRabbit review feedback for auth suggest-profile PR - Pass global flags (--base-path, --config, --config-path) through loadAuthManagerForList so config selection flags are respected - Fix misleading error log in deleteLegacyKeyringEntry to report actual keyring failures instead of "no legacy entry" - Replace hand-rolled realmAwareTestStore with generated MockCredentialStore Co-Authored-By: Claude Opus 4.6 --- cmd/auth_list.go | 17 +++++-- pkg/auth/realm_mismatch.go | 2 +- pkg/auth/realm_mismatch_test.go | 88 +++++---------------------------- 3 files changed, 27 insertions(+), 80 deletions(-) diff --git a/cmd/auth_list.go b/cmd/auth_list.go index bc8edde86f..a6ce9d1b9b 100644 --- a/cmd/auth_list.go +++ b/cmd/auth_list.go @@ -137,7 +137,7 @@ func executeAuthListCommand(cmd *cobra.Command, args []string) error { } // Load auth manager. - authManager, atmosConfig, err := loadAuthManagerForList() + authManager, atmosConfig, err := loadAuthManagerForList(cmd) if err != nil { return err } @@ -400,10 +400,21 @@ func suggestProfilesForAuth(atmosConfig *schema.AtmosConfiguration) error { } // loadAuthManagerForList loads the auth manager and returns the atmos config for profile discovery. -func loadAuthManagerForList() (authTypes.AuthManager, *schema.AtmosConfiguration, error) { +func loadAuthManagerForList(cmd *cobra.Command) (authTypes.AuthManager, *schema.AtmosConfiguration, error) { defer perf.Track(nil, "cmd.loadAuthManagerForList")() - atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false) + configAndStacksInfo := schema.ConfigAndStacksInfo{} + if bp, _ := cmd.Flags().GetString("base-path"); bp != "" { + configAndStacksInfo.AtmosBasePath = bp + } + if cfgFiles, _ := cmd.Flags().GetStringSlice("config"); len(cfgFiles) > 0 { + configAndStacksInfo.AtmosConfigFilesFromArg = cfgFiles + } + if cfgDirs, _ := cmd.Flags().GetStringSlice("config-path"); len(cfgDirs) > 0 { + configAndStacksInfo.AtmosConfigDirsFromArg = cfgDirs + } + + atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, false) if err != nil { return nil, nil, fmt.Errorf("%w: failed to load atmos config: %w", errUtils.ErrInvalidAuthConfig, err) } diff --git a/pkg/auth/realm_mismatch.go b/pkg/auth/realm_mismatch.go index 56a706dca4..e8054490c0 100644 --- a/pkg/auth/realm_mismatch.go +++ b/pkg/auth/realm_mismatch.go @@ -128,7 +128,7 @@ func (m *manager) deleteLegacyKeyringEntry(alias string) { return } if err := m.credentialStore.Delete(alias, ""); err != nil { - log.Debug("No legacy keyring entry to delete", "alias", alias) + log.Debug("Failed to delete legacy keyring entry", "alias", alias, "error", err) } else { log.Debug("Deleted legacy keyring entry (pre-realm)", "alias", alias) } diff --git a/pkg/auth/realm_mismatch_test.go b/pkg/auth/realm_mismatch_test.go index 5086482c81..874b45461f 100644 --- a/pkg/auth/realm_mismatch_test.go +++ b/pkg/auth/realm_mismatch_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/cloudposse/atmos/pkg/auth/realm" "github.com/cloudposse/atmos/pkg/auth/types" @@ -293,50 +294,32 @@ func TestCheckNoRealmCredentials(t *testing.T) { } func TestDeleteLegacyKeyringEntry_CleansUpEmptyRealm(t *testing.T) { - // Store has credentials under both realms. - store := &realmAwareTestStore{ - data: map[string]map[string]types.ICredentials{ - "my-identity": { - "": &testCreds{}, - "my-realm": &testCreds{}, - }, - }, - } + ctrl := gomock.NewController(t) + store := types.NewMockCredentialStore(ctrl) + + // Expect Delete to be called with the alias and empty realm (legacy entry). + store.EXPECT().Delete("my-identity", "").Return(nil) + m := &manager{ credentialStore: store, realm: realm.RealmInfo{Value: "my-realm", Source: "config"}, } m.deleteLegacyKeyringEntry("my-identity") - - // Legacy (empty-realm) entry should be deleted. - _, err := store.Retrieve("my-identity", "") - assert.Error(t, err, "legacy entry should have been deleted") - - // Current realm entry should be preserved. - _, err = store.Retrieve("my-identity", "my-realm") - assert.NoError(t, err, "current realm entry should remain") } func TestDeleteLegacyKeyringEntry_NoOpForEmptyRealm(t *testing.T) { - // When realm is empty, no cleanup should happen. - store := &realmAwareTestStore{ - data: map[string]map[string]types.ICredentials{ - "my-identity": { - "": &testCreds{}, - }, - }, - } + ctrl := gomock.NewController(t) + store := types.NewMockCredentialStore(ctrl) + + // No Delete call expected — realm is empty, so no cleanup should happen. + m := &manager{ credentialStore: store, realm: realm.RealmInfo{Value: "", Source: "auto"}, } m.deleteLegacyKeyringEntry("my-identity") - - // Entry should still exist — no cleanup for empty realm. - _, err := store.Retrieve("my-identity", "") - assert.NoError(t, err, "entry should not be deleted when realm is empty") } func TestDeleteLegacyKeyringEntry_NilStore(t *testing.T) { @@ -399,50 +382,3 @@ func TestLogRealmMismatchWarning_EmptyToNonEmpty(t *testing.T) { // Verify it doesn't panic. logRealmMismatchWarning("", "my-project") } - -// realmAwareTestStore is a test credential store that tracks realm for each entry. -// Unlike testStore, this distinguishes between credentials stored under different realms. -type realmAwareTestStore struct { - data map[string]map[string]types.ICredentials // alias -> realm -> creds. -} - -func (s *realmAwareTestStore) Store(alias string, creds types.ICredentials, realmValue string) error { - if s.data == nil { - s.data = map[string]map[string]types.ICredentials{} - } - if s.data[alias] == nil { - s.data[alias] = map[string]types.ICredentials{} - } - s.data[alias][realmValue] = creds - return nil -} - -func (s *realmAwareTestStore) Retrieve(alias string, realmValue string) (types.ICredentials, error) { - if s.data == nil { - return nil, assert.AnError - } - realms, ok := s.data[alias] - if !ok { - return nil, assert.AnError - } - creds, ok := realms[realmValue] - if !ok { - return nil, assert.AnError - } - return creds, nil -} - -func (s *realmAwareTestStore) Delete(alias string, realmValue string) error { - if s.data != nil { - if realms, ok := s.data[alias]; ok { - delete(realms, realmValue) - } - } - return nil -} - -func (s *realmAwareTestStore) List(realmValue string) ([]string, error) { return nil, nil } -func (s *realmAwareTestStore) IsExpired(alias string, realmValue string) (bool, error) { - return false, nil -} -func (s *realmAwareTestStore) Type() string { return "realm-aware-test" } From 0defba5c3c7905a13dd855e538779fa338c6b6ec Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Sun, 8 Mar 2026 13:49:47 -0500 Subject: [PATCH 4/9] fix: Correct docstrings for legacy cleanup functions in realm_mismatch.go Update deleteLegacyKeyringEntry and deleteLegacyCredentialFiles docstrings to accurately reflect that they run early in the Authenticate flow before authenticateChain, removing legacy entries unconditionally. Co-Authored-By: Claude Opus 4.6 --- pkg/auth/realm_mismatch.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/auth/realm_mismatch.go b/pkg/auth/realm_mismatch.go index e8054490c0..de00884d61 100644 --- a/pkg/auth/realm_mismatch.go +++ b/pkg/auth/realm_mismatch.go @@ -120,9 +120,11 @@ func hasCredentialFiles(awsDir string) bool { return false } -// deleteLegacyKeyringEntry removes a pre-realm keyring entry after credentials -// have been successfully stored under the current realm. This prevents the -// realm mismatch warning from firing on subsequent commands. +// deleteLegacyKeyringEntry removes a pre-realm keyring entry during the early +// cleanup phase of the Authenticate flow. This runs before authenticateChain, +// so legacy credentials are removed unconditionally even if subsequent +// authentication fails. This prevents the realm mismatch warning from firing +// on subsequent commands. func (m *manager) deleteLegacyKeyringEntry(alias string) { if m.realm.Value == "" || m.credentialStore == nil { return @@ -134,9 +136,11 @@ func (m *manager) deleteLegacyKeyringEntry(alias string) { } } -// deleteLegacyCredentialFiles removes pre-realm credential files after credentials -// have been successfully stored under the current realm. This prevents the file-based -// realm mismatch warning from firing on subsequent commands. +// deleteLegacyCredentialFiles removes pre-realm credential files during the early +// cleanup phase of the Authenticate flow. This runs before authenticateChain, +// so legacy files are removed unconditionally even if subsequent authentication +// fails. This prevents the file-based realm mismatch warning from firing on +// subsequent commands. // Scans {baseDir}/aws/{provider}/ for credential and config files and removes them. func (m *manager) deleteLegacyCredentialFiles() { if m.realm.Value == "" { From 47f2b435707c0986cf2236f2ea43c237f1efc482 Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Sun, 8 Mar 2026 18:01:27 -0500 Subject: [PATCH 5/9] fix: Address CodeRabbit review feedback for auth suggest-profile PR - Use unfiltered providers/identities for profile suggestion gate to avoid false positives when filters narrow results to empty - Move legacy credential cleanup to post-success path so fallback credentials remain if authentication fails - Add TODO for refactoring AWS-specific cleanup into provider-owned hook - Add tests for uncovered paths to improve patch coverage Co-Authored-By: Claude Opus 4.6 --- cmd/auth_list.go | 3 +- pkg/auth/manager.go | 16 +++---- pkg/auth/realm_mismatch.go | 20 ++++---- pkg/auth/realm_mismatch_test.go | 83 +++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/cmd/auth_list.go b/cmd/auth_list.go index a6ce9d1b9b..e708e147f8 100644 --- a/cmd/auth_list.go +++ b/cmd/auth_list.go @@ -154,7 +154,8 @@ func executeAuthListCommand(cmd *cobra.Command, args []string) error { // When no providers or identities are configured, check if profiles exist // that might contain auth configuration and suggest using --profile. - if len(filteredProviders) == 0 && len(filteredIdentities) == 0 { + // Use the unfiltered lists to avoid false positives when filters narrow results to empty. + if len(providers) == 0 && len(identities) == 0 { if suggestion := suggestProfilesForAuth(atmosConfig); suggestion != nil { return suggestion } diff --git a/pkg/auth/manager.go b/pkg/auth/manager.go index 8587cdd6bd..66322a4d39 100644 --- a/pkg/auth/manager.go +++ b/pkg/auth/manager.go @@ -231,14 +231,6 @@ func (m *manager) Authenticate(ctx context.Context, identityName string) (*types m.chain = chain log.Debug("Authentication chain discovered", logKeyIdentity, identityName, "chainLength", len(chain), "chain", chain) - // Clean up legacy (pre-realm) keyring and file entries to prevent realm mismatch warnings. - // This runs early in the Authenticate flow so it executes even when cached credentials - // allow the provider authentication step to be skipped. - m.deleteLegacyCredentialFiles() - for _, step := range chain { - m.deleteLegacyKeyringEntry(step) - } - // Perform credential chain authentication (bottom-up). finalCreds, err := m.authenticateChain(ctx, identityName) if err != nil { @@ -284,6 +276,14 @@ func (m *manager) Authenticate(ctx context.Context, identityName string) (*types m.triggerIntegrations(ctx, identityName, finalCreds) } + // Clean up legacy (pre-realm) keyring and file entries to prevent realm mismatch warnings. + // This runs after successful authentication so legacy credentials remain as a fallback + // if authentication fails. + m.deleteLegacyCredentialFiles() + for _, step := range chain { + m.deleteLegacyKeyringEntry(step) + } + return m.buildWhoamiInfo(identityName, finalCreds), nil } diff --git a/pkg/auth/realm_mismatch.go b/pkg/auth/realm_mismatch.go index de00884d61..4dcc351d26 100644 --- a/pkg/auth/realm_mismatch.go +++ b/pkg/auth/realm_mismatch.go @@ -120,11 +120,10 @@ func hasCredentialFiles(awsDir string) bool { return false } -// deleteLegacyKeyringEntry removes a pre-realm keyring entry during the early -// cleanup phase of the Authenticate flow. This runs before authenticateChain, -// so legacy credentials are removed unconditionally even if subsequent -// authentication fails. This prevents the realm mismatch warning from firing -// on subsequent commands. +// deleteLegacyKeyringEntry removes a pre-realm keyring entry after successful +// authentication. This runs in the post-success path of the Authenticate flow, +// after authenticateChain and PostAuthenticate have completed, so legacy +// credentials remain as a fallback if authentication fails. func (m *manager) deleteLegacyKeyringEntry(alias string) { if m.realm.Value == "" || m.credentialStore == nil { return @@ -136,12 +135,13 @@ func (m *manager) deleteLegacyKeyringEntry(alias string) { } } -// deleteLegacyCredentialFiles removes pre-realm credential files during the early -// cleanup phase of the Authenticate flow. This runs before authenticateChain, -// so legacy files are removed unconditionally even if subsequent authentication -// fails. This prevents the file-based realm mismatch warning from firing on -// subsequent commands. +// deleteLegacyCredentialFiles removes pre-realm credential files after successful +// authentication. This runs in the post-success path of the Authenticate flow, +// after authenticateChain and PostAuthenticate have completed, so legacy files +// remain as a fallback if authentication fails. // Scans {baseDir}/aws/{provider}/ for credential and config files and removes them. +// TODO: Refactor to use a provider/store-owned cleanup hook instead of hardcoding +// AWS-specific paths here, keeping the auth manager cloud-agnostic. func (m *manager) deleteLegacyCredentialFiles() { if m.realm.Value == "" { return diff --git a/pkg/auth/realm_mismatch_test.go b/pkg/auth/realm_mismatch_test.go index 874b45461f..6bb811a915 100644 --- a/pkg/auth/realm_mismatch_test.go +++ b/pkg/auth/realm_mismatch_test.go @@ -373,6 +373,89 @@ func TestDeleteLegacyCredentialFiles_NoOpForEmptyRealm(t *testing.T) { assert.NoError(t, err, "files should not be deleted when realm is empty") } +func TestDeleteLegacyKeyringEntry_DeleteFails(t *testing.T) { + ctrl := gomock.NewController(t) + store := types.NewMockCredentialStore(ctrl) + + // Expect Delete to be called but return an error. + store.EXPECT().Delete("my-identity", "").Return(assert.AnError) + + m := &manager{ + credentialStore: store, + realm: realm.RealmInfo{Value: "my-realm", Source: "config"}, + } + + // Should not panic even when Delete fails. + m.deleteLegacyKeyringEntry("my-identity") +} + +func TestDeleteLegacyCredentialFiles_CleansUpLockFilesAndEmptyDirs(t *testing.T) { + // Setup: credential files including lock files at {baseDir}/aws/{provider}/. + parent := t.TempDir() + atmosDir := filepath.Join(parent, "atmos") + providerDir := filepath.Join(atmosDir, "aws", "my-provider") + require.NoError(t, os.MkdirAll(providerDir, 0o700)) + require.NoError(t, os.WriteFile(filepath.Join(providerDir, "credentials"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(providerDir, "credentials.lock"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(providerDir, "config"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(providerDir, "config.lock"), []byte("test"), 0o600)) + t.Setenv("ATMOS_XDG_CONFIG_HOME", parent) + + m := &manager{ + realm: realm.RealmInfo{Value: "my-realm", Source: "config"}, + } + + m.deleteLegacyCredentialFiles() + + // All legacy files should be deleted. + for _, filename := range []string{"credentials", "credentials.lock", "config", "config.lock"} { + _, err := os.Stat(filepath.Join(providerDir, filename)) + assert.True(t, os.IsNotExist(err), "legacy %s file should be deleted", filename) + } + + // Provider and aws directories should be cleaned up since they're empty. + _, err := os.Stat(providerDir) + assert.True(t, os.IsNotExist(err), "empty provider dir should be removed") + _, err = os.Stat(filepath.Join(atmosDir, "aws")) + assert.True(t, os.IsNotExist(err), "empty aws dir should be removed") +} + +func TestDeleteLegacyCredentialFiles_SkipsNonDirectoryEntries(t *testing.T) { + // Setup: aws dir with a regular file (not a provider directory). + parent := t.TempDir() + atmosDir := filepath.Join(parent, "atmos") + awsDir := filepath.Join(atmosDir, "aws") + require.NoError(t, os.MkdirAll(awsDir, 0o700)) + // Create a stray file in the aws dir (should be skipped). + require.NoError(t, os.WriteFile(filepath.Join(awsDir, "stray-file.txt"), []byte("not a dir"), 0o600)) + t.Setenv("ATMOS_XDG_CONFIG_HOME", parent) + + m := &manager{ + realm: realm.RealmInfo{Value: "my-realm", Source: "config"}, + } + + // Should not panic, stray file should remain. + m.deleteLegacyCredentialFiles() + + _, err := os.Stat(filepath.Join(awsDir, "stray-file.txt")) + assert.NoError(t, err, "non-directory entries should not be removed") +} + +func TestDeleteLegacyCredentialFiles_NoAwsDir(t *testing.T) { + // Setup: base dir exists but no aws subdirectory. + parent := t.TempDir() + atmosDir := filepath.Join(parent, "atmos") + require.NoError(t, os.MkdirAll(atmosDir, 0o700)) + t.Setenv("ATMOS_XDG_CONFIG_HOME", parent) + + m := &manager{ + realm: realm.RealmInfo{Value: "my-realm", Source: "config"}, + } + + // Should not panic. + m.deleteLegacyCredentialFiles() +} + func TestLogRealmMismatchWarning_NonEmptyToEmpty(t *testing.T) { // Verify it doesn't panic. The actual log output goes to the logger. logRealmMismatchWarning("my-realm", "(no realm)") From c9b919bc061e91879a2d14ca6597ddd7cff89672 Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Tue, 10 Mar 2026 15:19:46 -0500 Subject: [PATCH 6/9] fix: Escalate legacy keyring delete errors from Debug to Warn Store implementations already handle not-found internally (returning nil), so any error reaching deleteLegacyKeyringEntry is a real backend failure that deserves Warn-level visibility. Co-Authored-By: Claude Opus 4.6 --- pkg/auth/realm_mismatch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/auth/realm_mismatch.go b/pkg/auth/realm_mismatch.go index 4dcc351d26..7c43fe3c49 100644 --- a/pkg/auth/realm_mismatch.go +++ b/pkg/auth/realm_mismatch.go @@ -129,7 +129,7 @@ func (m *manager) deleteLegacyKeyringEntry(alias string) { return } if err := m.credentialStore.Delete(alias, ""); err != nil { - log.Debug("Failed to delete legacy keyring entry", "alias", alias, "error", err) + log.Warn("Failed to delete legacy keyring entry", "alias", alias, "error", err) } else { log.Debug("Deleted legacy keyring entry (pre-realm)", "alias", alias) } From 76f47d498f369a35d39d67944feae3664d772ac3 Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Fri, 13 Mar 2026 17:38:25 -0500 Subject: [PATCH 7/9] fix: Improve error message when identity is not configured in active profile Replace the confusing "unsupported identity kind: " error (with dangling colon) with context-aware messages that explain the actual problem and suggest next steps. With a profile active: "Identity is not configured in the `marketplace` profile." Without a profile: "Identity is not configured. Did you forget to specify a profile?" Co-Authored-By: Claude Opus 4.6 --- pkg/auth/factory/factory.go | 3 ++ pkg/auth/factory/factory_test.go | 5 ++ pkg/auth/manager.go | 23 +++++++++ pkg/auth/manager_test.go | 84 ++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+) diff --git a/pkg/auth/factory/factory.go b/pkg/auth/factory/factory.go index bc861ca18a..179131a7ff 100644 --- a/pkg/auth/factory/factory.go +++ b/pkg/auth/factory/factory.go @@ -158,6 +158,9 @@ func NewIdentity(name string, config *schema.Identity) (types.Identity, error) { case "mock/aws": return mockawsProviders.NewIdentity(name, config), nil default: + if config.Kind == "" { + return nil, fmt.Errorf("%w: identity is not configured", errUtils.ErrInvalidIdentityKind) + } return nil, fmt.Errorf("%w: unsupported identity kind: %s", errUtils.ErrInvalidIdentityKind, config.Kind) } } diff --git a/pkg/auth/factory/factory_test.go b/pkg/auth/factory/factory_test.go index c439f8cf74..c8fdb056e5 100644 --- a/pkg/auth/factory/factory_test.go +++ b/pkg/auth/factory/factory_test.go @@ -165,6 +165,7 @@ func TestNewIdentity_Factory(t *testing.T) { config *schema.Identity expectError bool errorType error + errorMsg string }{ { name: "aws-permission-set-valid", @@ -257,6 +258,7 @@ func TestNewIdentity_Factory(t *testing.T) { config: &schema.Identity{Kind: ""}, expectError: true, errorType: errUtils.ErrInvalidIdentityKind, + errorMsg: "identity is not configured", }, { name: "empty-name-allowed", @@ -275,6 +277,9 @@ func TestNewIdentity_Factory(t *testing.T) { if tt.errorType != nil { assert.ErrorIs(t, err, tt.errorType) } + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg) + } assert.Nil(t, identity) } else { assert.NoError(t, err) diff --git a/pkg/auth/manager.go b/pkg/auth/manager.go index 66322a4d39..b9130a75fd 100644 --- a/pkg/auth/manager.go +++ b/pkg/auth/manager.go @@ -580,6 +580,29 @@ func (m *manager) initializeProviders() error { // legacy path behavior with no realm subdirectory. func (m *manager) initializeIdentities() error { for name, identityConfig := range m.config.Identities { + // Check for unconfigured identities (empty kind) before attempting factory creation. + // This produces a clear, actionable error instead of the confusing "unsupported identity kind: ". + if identityConfig.Kind == "" { + builder := errUtils.Build(errUtils.ErrInvalidIdentityConfig). + WithContext("identity", name) + + if m.stackInfo != nil && len(m.stackInfo.ProfilesFromArg) > 0 { + profileNames := strings.Join(m.stackInfo.ProfilesFromArg, ", ") + builder = builder. + WithExplanationf("Identity %q is not configured in the `%s` profile.", name, profileNames). + WithHint("Switch to a profile that includes this identity") + } else { + builder = builder. + WithExplanationf("Identity %q is not configured. Did you forget to specify a profile?", name) + } + + err := builder. + WithHint("Run `atmos profile list` to see available profiles"). + WithExitCode(1).Err() + errUtils.CheckErrorAndPrint(err, "Initialize Identities", "") + return err + } + identity, err := factory.NewIdentity(name, &identityConfig) if err != nil { errUtils.CheckErrorAndPrint(err, "Initialize Identities", "") diff --git a/pkg/auth/manager_test.go b/pkg/auth/manager_test.go index df5165789a..d5c384f65b 100644 --- a/pkg/auth/manager_test.go +++ b/pkg/auth/manager_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + cockroachErrors "github.com/cockroachdb/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -2150,3 +2151,86 @@ func TestManager_ResolveProviderConfig(t *testing.T) { }) } } + +func TestInitializeIdentities_EmptyKindWithProfile(t *testing.T) { + m := &manager{ + config: &schema.AuthConfig{ + Identities: map[string]schema.Identity{ + "core-root/terraform": {Kind: ""}, // Empty kind - not configured in profile. + }, + }, + identities: make(map[string]types.Identity), + stackInfo: &schema.ConfigAndStacksInfo{ + ProfilesFromArg: []string{"marketplace"}, + }, + } + + err := m.initializeIdentities() + assert.Error(t, err) + assert.ErrorIs(t, err, errUtils.ErrInvalidIdentityConfig) + + // Explanation is stored as a detail on the structured error. + details := cockroachErrors.GetAllDetails(err) + require.NotEmpty(t, details) + assert.Contains(t, details[0], "core-root/terraform") + assert.Contains(t, details[0], "is not configured in the") + assert.Contains(t, details[0], "marketplace") +} + +func TestInitializeIdentities_EmptyKindWithoutProfile(t *testing.T) { + m := &manager{ + config: &schema.AuthConfig{ + Identities: map[string]schema.Identity{ + "core-root/terraform": {Kind: ""}, // Empty kind - not configured. + }, + }, + identities: make(map[string]types.Identity), + stackInfo: &schema.ConfigAndStacksInfo{}, + } + + err := m.initializeIdentities() + assert.Error(t, err) + assert.ErrorIs(t, err, errUtils.ErrInvalidIdentityConfig) + + details := cockroachErrors.GetAllDetails(err) + require.NotEmpty(t, details) + assert.Contains(t, details[0], "core-root/terraform") + assert.Contains(t, details[0], "is not configured") + assert.Contains(t, details[0], "Did you forget to specify a profile?") +} + +func TestInitializeIdentities_EmptyKindNilStackInfo(t *testing.T) { + m := &manager{ + config: &schema.AuthConfig{ + Identities: map[string]schema.Identity{ + "core-root/terraform": {Kind: ""}, + }, + }, + identities: make(map[string]types.Identity), + stackInfo: nil, + } + + err := m.initializeIdentities() + assert.Error(t, err) + assert.ErrorIs(t, err, errUtils.ErrInvalidIdentityConfig) + + details := cockroachErrors.GetAllDetails(err) + require.NotEmpty(t, details) + assert.Contains(t, details[0], "Did you forget to specify a profile?") +} + +func TestInitializeIdentities_ValidKindSucceeds(t *testing.T) { + m := &manager{ + config: &schema.AuthConfig{ + Identities: map[string]schema.Identity{ + "mock-identity": {Kind: "mock"}, + }, + }, + identities: make(map[string]types.Identity), + stackInfo: &schema.ConfigAndStacksInfo{}, + } + + err := m.initializeIdentities() + assert.NoError(t, err) + assert.Len(t, m.identities, 1) +} From 30876e69597080c60d6860232fd782835a3a7e8b Mon Sep 17 00:00:00 2001 From: aknysh Date: Mon, 16 Mar 2026 12:17:16 -0400 Subject: [PATCH 8/9] fix: propagate auth realm through CopyGlobalAuthConfig and buildGlobalAuthSection (#2187) When running `atmos terraform plan -s --all`, the realm was lost because CopyGlobalAuthConfig omitted Realm/RealmSource fields and buildGlobalAuthSection omitted realm from the auth map. This caused credentials to be stored under the wrong path, triggering realm mismatch warnings. Co-Authored-By: Claude Opus 4.6 --- internal/exec/utils_auth.go | 3 + internal/exec/utils_auth_test.go | 169 ++++++++++++++++++ pkg/auth/config_helpers.go | 4 +- pkg/auth/config_helpers_test.go | 145 ++++++++++++++- ...-s_dev_(stack-names_example).stdout.golden | 4 +- ...uction_(stack-names_example).stdout.golden | 4 +- ...v_(native-terraform_example).stdout.golden | 4 +- ..._vpc_-s_my-legacy-prod-stack.stdout.golden | 4 +- ...omponent_vpc_-s_no-name-prod.stdout.golden | 4 +- ...n_(native-terraform_example).stdout.golden | 4 +- ...scovers_atmos.yaml_in_parent.stdout.golden | 3 +- ...ame_(backward_compatibility).stdout.golden | 3 +- ...t_with_current_directory_(.).stdout.golden | 3 +- ...component_with_relative_path.stdout.golden | 3 +- ...be_component_with_stack_flag.stdout.golden | 3 +- 15 files changed, 347 insertions(+), 13 deletions(-) diff --git a/internal/exec/utils_auth.go b/internal/exec/utils_auth.go index b5a194d4ee..e7a7d29112 100644 --- a/internal/exec/utils_auth.go +++ b/internal/exec/utils_auth.go @@ -188,6 +188,9 @@ func buildGlobalAuthSection(atmosConfig *schema.AtmosConfiguration) map[string]a if atmosConfig.Auth.Keyring.Type != "" { globalAuthSection["keyring"] = atmosConfig.Auth.Keyring } + if atmosConfig.Auth.Realm != "" { + globalAuthSection["realm"] = atmosConfig.Auth.Realm + } return globalAuthSection } diff --git a/internal/exec/utils_auth_test.go b/internal/exec/utils_auth_test.go index a0a5186d45..a286020127 100644 --- a/internal/exec/utils_auth_test.go +++ b/internal/exec/utils_auth_test.go @@ -148,6 +148,56 @@ func TestBuildGlobalAuthSection(t *testing.T) { "keyring": schema.KeyringConfig{Type: "file"}, }, }, + { + name: "realm included when set", + config: &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "my-project", + }, + }, + expected: map[string]any{ + "realm": "my-project", + }, + }, + { + name: "realm excluded when empty", + config: &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "", + }, + }, + expected: map[string]any{}, + }, + { + name: "all sections including realm", + config: &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "prod-realm", + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws-iam"}, + }, + Identities: map[string]schema.Identity{ + "dev": {Kind: "aws"}, + }, + Logs: schema.Logs{Level: "info"}, + Keyring: schema.KeyringConfig{Type: "file"}, + }, + }, + expected: map[string]any{ + "realm": "prod-realm", + "providers": map[string]schema.Provider{ + "aws": {Kind: "aws-iam"}, + }, + "identities": map[string]schema.Identity{ + "dev": {Kind: "aws"}, + }, + "logs": map[string]any{ + "level": "info", + "file": "", + }, + "keyring": schema.KeyringConfig{Type: "file"}, + }, + }, { name: "empty maps are excluded", config: &schema.AtmosConfiguration{ @@ -715,6 +765,125 @@ func TestCreateAndAuthenticateAuthManagerWithDeps_NilAuthManager(t *testing.T) { assert.Nil(t, result) } +func TestGetMergedAuthConfigWithFetcher_RealmPropagated(t *testing.T) { + // Verify realm is propagated through CopyGlobalAuthConfig when no component auth exists. + // This is the --all path: each component iteration must preserve the realm. + atmosConfig := &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "my-project", + RealmSource: "config", + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws-iam"}, + }, + }, + } + info := &schema.ConfigAndStacksInfo{ + Stack: "dev-us-west-2", + ComponentFromArg: "vpc", + } + + // Mock fetcher returns component config without auth section. + mockFetcher := func(_ *ExecuteDescribeComponentParams) (map[string]any, error) { + return map[string]any{ + "vars": map[string]any{"test": "value"}, + }, nil + } + + result, err := getMergedAuthConfigWithFetcher(atmosConfig, info, mockFetcher) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "my-project", result.Realm) + assert.Equal(t, "config", result.RealmSource) +} + +func TestGetMergedAuthConfigWithFetcher_RealmPropagatedWithEmptyStack(t *testing.T) { + // When stack is empty (global auth only path), realm must still be preserved. + atmosConfig := &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "env-realm", + RealmSource: "env", + }, + } + info := &schema.ConfigAndStacksInfo{ + Stack: "", + ComponentFromArg: "", + } + + mockFetcher := func(_ *ExecuteDescribeComponentParams) (map[string]any, error) { + t.Fatal("fetcher should not be called when stack is empty") + return nil, nil + } + + result, err := getMergedAuthConfigWithFetcher(atmosConfig, info, mockFetcher) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "env-realm", result.Realm) + assert.Equal(t, "env", result.RealmSource) +} + +func TestMergeGlobalAuthConfig_RealmPropagated(t *testing.T) { + // Verify realm is included in the merged auth section map. + atmosConfig := &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "my-project", + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws-iam"}, + }, + }, + } + componentSection := map[string]any{} + + result := mergeGlobalAuthConfig(atmosConfig, componentSection) + assert.Contains(t, result, "realm") + assert.Equal(t, "my-project", result["realm"]) + assert.Contains(t, result, "providers") +} + +func TestMergeGlobalAuthConfig_NoRealmConfigured(t *testing.T) { + // When no realm is configured, the merged map should not contain a "realm" key. + atmosConfig := &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws-iam"}, + }, + }, + } + componentSection := map[string]any{} + + result := mergeGlobalAuthConfig(atmosConfig, componentSection) + assert.NotContains(t, result, "realm") + assert.Contains(t, result, "providers") +} + +func TestGetMergedAuthConfigWithFetcher_NoRealmPreservesEmptyRealm(t *testing.T) { + // When no realm is configured, the merged config should have empty realm — same as before the fix. + atmosConfig := &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws-iam"}, + }, + }, + } + info := &schema.ConfigAndStacksInfo{ + Stack: "dev-us-west-2", + ComponentFromArg: "vpc", + } + + mockFetcher := func(_ *ExecuteDescribeComponentParams) (map[string]any, error) { + return map[string]any{ + "vars": map[string]any{"test": "value"}, + }, nil + } + + result, err := getMergedAuthConfigWithFetcher(atmosConfig, info, mockFetcher) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result.Realm) + assert.Empty(t, result.RealmSource) + // Providers should still be present. + assert.Len(t, result.Providers, 1) +} + func TestGetMergedAuthConfigWithFetcher_MergeReturnsError(t *testing.T) { // This test verifies the path where MergeComponentAuthFromConfig returns an error. // When that happens, getMergedAuthConfigWithFetcher propagates the error. diff --git a/pkg/auth/config_helpers.go b/pkg/auth/config_helpers.go index 86680731f5..52612e6819 100644 --- a/pkg/auth/config_helpers.go +++ b/pkg/auth/config_helpers.go @@ -19,7 +19,9 @@ func CopyGlobalAuthConfig(globalAuth *schema.AuthConfig) *schema.AuthConfig { } config := &schema.AuthConfig{ - Logs: globalAuth.Logs, + Realm: globalAuth.Realm, + RealmSource: globalAuth.RealmSource, + Logs: globalAuth.Logs, Keyring: schema.KeyringConfig{ Type: globalAuth.Keyring.Type, }, diff --git a/pkg/auth/config_helpers_test.go b/pkg/auth/config_helpers_test.go index d90027828c..12ff2174e0 100644 --- a/pkg/auth/config_helpers_test.go +++ b/pkg/auth/config_helpers_test.go @@ -25,8 +25,10 @@ func TestCopyGlobalAuthConfig(t *testing.T) { }, }, { - name: "copies all fields", + name: "copies all fields including realm", globalAuth: &schema.AuthConfig{ + Realm: "my-project", + RealmSource: "config", Providers: map[string]schema.Provider{ "test-provider": { Kind: "aws/iam-identity-center", @@ -50,6 +52,8 @@ func TestCopyGlobalAuthConfig(t *testing.T) { }, }, verify: func(t *testing.T, result *schema.AuthConfig) { + assert.Equal(t, "my-project", result.Realm) + assert.Equal(t, "config", result.RealmSource) assert.Len(t, result.Providers, 1) assert.Contains(t, result.Providers, "test-provider") assert.Len(t, result.Identities, 1) @@ -59,6 +63,28 @@ func TestCopyGlobalAuthConfig(t *testing.T) { assert.Len(t, result.IdentityCaseMap, 1) }, }, + { + name: "copies realm from env source", + globalAuth: &schema.AuthConfig{ + Realm: "env-realm", + RealmSource: "env", + }, + verify: func(t *testing.T, result *schema.AuthConfig) { + assert.Equal(t, "env-realm", result.Realm) + assert.Equal(t, "env", result.RealmSource) + }, + }, + { + name: "empty realm is preserved", + globalAuth: &schema.AuthConfig{ + Realm: "", + RealmSource: "", + }, + verify: func(t *testing.T, result *schema.AuthConfig) { + assert.Empty(t, result.Realm) + assert.Empty(t, result.RealmSource) + }, + }, { name: "deep copies Keyring.Spec map", globalAuth: &schema.AuthConfig{ @@ -91,6 +117,8 @@ func TestCopyGlobalAuthConfig(t *testing.T) { func TestCopyGlobalAuthConfig_DeepCopyMutation(t *testing.T) { // Test that modifying the copy doesn't mutate the original. original := &schema.AuthConfig{ + Realm: "original-realm", + RealmSource: "config", Keyring: schema.KeyringConfig{ Type: "file", Spec: map[string]interface{}{ @@ -106,12 +134,16 @@ func TestCopyGlobalAuthConfig_DeepCopyMutation(t *testing.T) { copy := CopyGlobalAuthConfig(original) // Modify the copy. + copy.Realm = "modified-realm" + copy.RealmSource = "env" copy.Keyring.Spec["path"] = "/modified/path" copy.Keyring.Spec["new_key"] = "new_value" copy.IdentityCaseMap["original"] = "Modified" copy.IdentityCaseMap["new"] = "New" // Verify original is unchanged. + assert.Equal(t, "original-realm", original.Realm) + assert.Equal(t, "config", original.RealmSource) assert.Equal(t, "/original/path", original.Keyring.Spec["path"]) assert.Len(t, original.Keyring.Spec, 1) assert.NotContains(t, original.Keyring.Spec, "new_key") @@ -317,6 +349,117 @@ func TestMergeComponentAuthFromConfig_NilGlobalAuth(t *testing.T) { } } +func TestMergeComponentAuthFromConfig_RealmSurvivesMapstructureRoundTrip(t *testing.T) { + // Realm has mapstructure:"realm" so it must survive the struct→map→merge→map→struct round-trip. + // RealmSource has mapstructure:"-" so it is lost — this is expected because GetRealm() recomputes it. + globalAuth := &schema.AuthConfig{ + Realm: "my-project", + RealmSource: "config", + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws/iam-identity-center", Region: "us-east-1"}, + }, + Identities: map[string]schema.Identity{ + "global-id": {Kind: "aws/user", Default: true}, + }, + IdentityCaseMap: map[string]string{ + "global-id": "global-id", + }, + } + + atmosConfig := &schema.AtmosConfiguration{ + Settings: schema.AtmosSettings{ + ListMergeStrategy: "replace", + }, + } + + // Component with its own auth section that overrides some fields. + componentConfig := map[string]any{ + cfg.AuthSectionName: map[string]any{ + "identities": map[string]any{ + "component-id": map[string]any{ + "kind": "aws/assume-role", + }, + }, + }, + } + + result, err := MergeComponentAuthFromConfig(globalAuth, componentConfig, atmosConfig, cfg.AuthSectionName) + assert.NoError(t, err) + assert.NotNil(t, result) + + // Realm must survive the merge round-trip. + assert.Equal(t, "my-project", result.Realm) + + // RealmSource is lost (mapstructure:"-") — this is expected. + // GetRealm() recomputes it from the Realm value when creating the manager. + assert.Empty(t, result.RealmSource) + + // Other fields still work. + assert.Len(t, result.Identities, 2) + assert.Contains(t, result.Identities, "global-id") + assert.Contains(t, result.Identities, "component-id") +} + +func TestMergeComponentAuthFromConfig_NoRealmConfigured(t *testing.T) { + // When no realm is configured, everything should work as before — no realm in result. + globalAuth := &schema.AuthConfig{ + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws/iam-identity-center"}, + }, + Identities: map[string]schema.Identity{ + "dev": {Kind: "aws/user", Default: true}, + }, + IdentityCaseMap: map[string]string{ + "dev": "dev", + }, + } + + atmosConfig := &schema.AtmosConfiguration{ + Settings: schema.AtmosSettings{ + ListMergeStrategy: "replace", + }, + } + + componentConfig := map[string]any{ + cfg.AuthSectionName: map[string]any{ + "identities": map[string]any{ + "staging": map[string]any{ + "kind": "aws/assume-role", + }, + }, + }, + } + + result, err := MergeComponentAuthFromConfig(globalAuth, componentConfig, atmosConfig, cfg.AuthSectionName) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result.Realm) + assert.Empty(t, result.RealmSource) + assert.Len(t, result.Identities, 2) +} + +func TestCopyGlobalAuthConfig_NoRealmConfigured(t *testing.T) { + // When no realm is configured at all, CopyGlobalAuthConfig should produce a valid config + // with empty realm fields — same as before this fix was applied. + globalAuth := &schema.AuthConfig{ + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws/iam-identity-center"}, + }, + Identities: map[string]schema.Identity{ + "dev": {Kind: "aws/user", Default: true}, + }, + Logs: schema.Logs{Level: "Info"}, + } + + result := CopyGlobalAuthConfig(globalAuth) + assert.NotNil(t, result) + assert.Empty(t, result.Realm) + assert.Empty(t, result.RealmSource) + assert.Len(t, result.Providers, 1) + assert.Len(t, result.Identities, 1) + assert.Equal(t, "Info", result.Logs.Level) +} + func TestAuthConfigToMap(t *testing.T) { tests := []struct { name string diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_dev_(stack-names_example).stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_dev_(stack-names_example).stdout.golden index fb0ad9e84b..9e2b28ab8c 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_dev_(stack-names_example).stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_dev_(stack-names_example).stdout.golden @@ -75,7 +75,9 @@ "atmos_manifest": "dev", "atmos_stack": "dev", "atmos_stack_file": "dev", - "auth": {}, + "auth": { + "realm": "178007337c087aed" + }, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_production_(stack-names_example).stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_production_(stack-names_example).stdout.golden index c5db9c607b..5b485bec0d 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_production_(stack-names_example).stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_production_(stack-names_example).stdout.golden @@ -75,7 +75,9 @@ "atmos_manifest": "prod", "atmos_stack": "production", "atmos_stack_file": "prod", - "auth": {}, + "auth": { + "realm": "178007337c087aed" + }, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_dev_(native-terraform_example).stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_dev_(native-terraform_example).stdout.golden index 6efd0b7941..0e025e191e 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_dev_(native-terraform_example).stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_dev_(native-terraform_example).stdout.golden @@ -77,7 +77,9 @@ "atmos_manifest": "dev", "atmos_stack": "dev", "atmos_stack_file": "dev", - "auth": {}, + "auth": { + "realm": "7bb257e4fdba764d" + }, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_my-legacy-prod-stack.stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_my-legacy-prod-stack.stdout.golden index 31bcb1c3c5..6e863507aa 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_my-legacy-prod-stack.stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_my-legacy-prod-stack.stdout.golden @@ -77,7 +77,9 @@ "atmos_manifest": "legacy-prod", "atmos_stack": "my-legacy-prod-stack", "atmos_stack_file": "legacy-prod", - "auth": {}, + "auth": { + "realm": "04bfae195bf1970c" + }, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_no-name-prod.stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_no-name-prod.stdout.golden index 1837d11468..87469f96dc 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_no-name-prod.stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_no-name-prod.stdout.golden @@ -77,7 +77,9 @@ "atmos_manifest": "no-name-prod", "atmos_stack": "no-name-prod", "atmos_stack_file": "no-name-prod", - "auth": {}, + "auth": { + "realm": "04bfae195bf1970c" + }, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_production_(native-terraform_example).stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_production_(native-terraform_example).stdout.golden index 97710fe8f5..1a97892d29 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_production_(native-terraform_example).stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_production_(native-terraform_example).stdout.golden @@ -77,7 +77,9 @@ "atmos_manifest": "prod", "atmos_stack": "production", "atmos_stack_file": "prod", - "auth": {}, + "auth": { + "realm": "7bb257e4fdba764d" + }, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_describe_component_from_nested_dir_discovers_atmos.yaml_in_parent.stdout.golden b/tests/snapshots/TestCLICommands_describe_component_from_nested_dir_discovers_atmos.yaml_in_parent.stdout.golden index 7053583b5f..5846979970 100644 --- a/tests/snapshots/TestCLICommands_describe_component_from_nested_dir_discovers_atmos.yaml_in_parent.stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_from_nested_dir_discovers_atmos.yaml_in_parent.stdout.golden @@ -51,7 +51,8 @@ atmos_component: test-component atmos_manifest: test-stack atmos_stack: test atmos_stack_file: test-stack -auth: {} +auth: + realm: a14b6bb632762f7d backend: {} backend_type: "" cli_args: diff --git a/tests/snapshots/TestCLICommands_describe_component_with_component_name_(backward_compatibility).stdout.golden b/tests/snapshots/TestCLICommands_describe_component_with_component_name_(backward_compatibility).stdout.golden index 695a23a87e..5b902298ef 100644 --- a/tests/snapshots/TestCLICommands_describe_component_with_component_name_(backward_compatibility).stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_with_component_name_(backward_compatibility).stdout.golden @@ -53,7 +53,8 @@ atmos_component: top-level-component1 atmos_manifest: orgs/cp/tenant1/dev/us-east-2 atmos_stack: tenant1-ue2-dev atmos_stack_file: orgs/cp/tenant1/dev/us-east-2 -auth: {} +auth: + realm: b80ea18be93f8201 backend: acl: bucket-owner-full-control bucket: cp-ue2-root-tfstate diff --git a/tests/snapshots/TestCLICommands_describe_component_with_current_directory_(.).stdout.golden b/tests/snapshots/TestCLICommands_describe_component_with_current_directory_(.).stdout.golden index 4798a136ca..5aba72fee2 100644 --- a/tests/snapshots/TestCLICommands_describe_component_with_current_directory_(.).stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_with_current_directory_(.).stdout.golden @@ -53,7 +53,8 @@ atmos_component: top-level-component1 atmos_manifest: orgs/cp/tenant1/dev/us-east-2 atmos_stack: tenant1-ue2-dev atmos_stack_file: orgs/cp/tenant1/dev/us-east-2 -auth: {} +auth: + realm: b80ea18be93f8201 backend: acl: bucket-owner-full-control bucket: cp-ue2-root-tfstate diff --git a/tests/snapshots/TestCLICommands_describe_component_with_relative_path.stdout.golden b/tests/snapshots/TestCLICommands_describe_component_with_relative_path.stdout.golden index 4798a136ca..5aba72fee2 100644 --- a/tests/snapshots/TestCLICommands_describe_component_with_relative_path.stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_with_relative_path.stdout.golden @@ -53,7 +53,8 @@ atmos_component: top-level-component1 atmos_manifest: orgs/cp/tenant1/dev/us-east-2 atmos_stack: tenant1-ue2-dev atmos_stack_file: orgs/cp/tenant1/dev/us-east-2 -auth: {} +auth: + realm: b80ea18be93f8201 backend: acl: bucket-owner-full-control bucket: cp-ue2-root-tfstate diff --git a/tests/snapshots/TestCLICommands_describe_component_with_stack_flag.stdout.golden b/tests/snapshots/TestCLICommands_describe_component_with_stack_flag.stdout.golden index 6d28e681f2..bd236dfe54 100644 --- a/tests/snapshots/TestCLICommands_describe_component_with_stack_flag.stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_with_stack_flag.stdout.golden @@ -52,7 +52,8 @@ atmos_component: vpc atmos_manifest: orgs/acme/plat/dev/us-east-2 atmos_stack: plat-ue2-dev atmos_stack_file: orgs/acme/plat/dev/us-east-2 -auth: {} +auth: + realm: b14b9545124ec327 backend: {} backend_type: "" cli_args: From 6599e73fc48aba8a0ac66dbf7ac0485f6e48bd36 Mon Sep 17 00:00:00 2001 From: aknysh Date: Mon, 16 Mar 2026 13:39:51 -0400 Subject: [PATCH 9/9] fix: address CodeRabbit review feedback for auth realm propagation - Use flags.ParseGlobalFlags in loadAuthManagerForList so --profile and other global selectors are respected instead of hand-rolled flag extraction - Add --profile regression test confirming profile auth config suppresses ErrAuthNotConfigured - Only include realm in buildGlobalAuthSection when explicitly configured (env/config), excluding auto-computed config-path hashes that are machine-specific and break CI snapshots - Guard err.Error() in factory_test.go to prevent panic on nil error - Log unexpected file cleanup failures in deleteLegacyCredentialFiles instead of silently ignoring them - Consolidate identity initialization tests into table-driven format - Restore golden snapshots to exclude path-dependent realm hashes Co-Authored-By: Claude Opus 4.6 --- cmd/auth_list.go | 18 +-- cmd/auth_list_test.go | 58 ++++++++ internal/exec/utils_auth.go | 6 +- internal/exec/utils_auth_test.go | 65 ++++++++- pkg/auth/factory/factory_test.go | 2 +- pkg/auth/manager_test.go | 132 ++++++++---------- pkg/auth/realm_mismatch.go | 37 +++-- ...-s_dev_(stack-names_example).stdout.golden | 4 +- ...uction_(stack-names_example).stdout.golden | 4 +- ...v_(native-terraform_example).stdout.golden | 4 +- ..._vpc_-s_my-legacy-prod-stack.stdout.golden | 4 +- ...omponent_vpc_-s_no-name-prod.stdout.golden | 4 +- ...n_(native-terraform_example).stdout.golden | 4 +- ...scovers_atmos.yaml_in_parent.stdout.golden | 3 +- ...ame_(backward_compatibility).stdout.golden | 3 +- ...t_with_current_directory_(.).stdout.golden | 3 +- ...component_with_relative_path.stdout.golden | 3 +- ...be_component_with_stack_flag.stdout.golden | 3 +- 18 files changed, 230 insertions(+), 127 deletions(-) diff --git a/cmd/auth_list.go b/cmd/auth_list.go index e708e147f8..858b5eb1bc 100644 --- a/cmd/auth_list.go +++ b/cmd/auth_list.go @@ -8,12 +8,14 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" "gopkg.in/yaml.v3" errUtils "github.com/cloudposse/atmos/errors" authList "github.com/cloudposse/atmos/pkg/auth/list" authTypes "github.com/cloudposse/atmos/pkg/auth/types" cfg "github.com/cloudposse/atmos/pkg/config" + "github.com/cloudposse/atmos/pkg/flags" log "github.com/cloudposse/atmos/pkg/logger" "github.com/cloudposse/atmos/pkg/perf" "github.com/cloudposse/atmos/pkg/profile" @@ -404,15 +406,13 @@ func suggestProfilesForAuth(atmosConfig *schema.AtmosConfiguration) error { func loadAuthManagerForList(cmd *cobra.Command) (authTypes.AuthManager, *schema.AtmosConfiguration, error) { defer perf.Track(nil, "cmd.loadAuthManagerForList")() - configAndStacksInfo := schema.ConfigAndStacksInfo{} - if bp, _ := cmd.Flags().GetString("base-path"); bp != "" { - configAndStacksInfo.AtmosBasePath = bp - } - if cfgFiles, _ := cmd.Flags().GetStringSlice("config"); len(cfgFiles) > 0 { - configAndStacksInfo.AtmosConfigFilesFromArg = cfgFiles - } - if cfgDirs, _ := cmd.Flags().GetStringSlice("config-path"); len(cfgDirs) > 0 { - configAndStacksInfo.AtmosConfigDirsFromArg = cfgDirs + v := viper.GetViper() + globalFlags := flags.ParseGlobalFlags(cmd, v) + configAndStacksInfo := schema.ConfigAndStacksInfo{ + AtmosBasePath: globalFlags.BasePath, + AtmosConfigFilesFromArg: globalFlags.Config, + AtmosConfigDirsFromArg: globalFlags.ConfigPath, + ProfilesFromArg: globalFlags.Profile, } atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, false) diff --git a/cmd/auth_list_test.go b/cmd/auth_list_test.go index 610fd7e3a7..b95264a2aa 100644 --- a/cmd/auth_list_test.go +++ b/cmd/auth_list_test.go @@ -8,6 +8,7 @@ import ( cockroachErrors "github.com/cockroachdb/errors" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -509,6 +510,63 @@ func TestExecuteAuthListCommand_NoErrorWhenNoProfilesExist(t *testing.T) { assert.NoError(t, err) } +func TestExecuteAuthListCommand_ProfileFlagSuppressesNoAuthError(t *testing.T) { + // Regression test: when --profile loads a profile that contains auth config, + // ErrAuthNotConfigured must NOT be returned, even though the base atmos.yaml + // has no auth providers/identities. + tempDir := t.TempDir() + + // Base atmos.yaml: no auth providers/identities. + baseConfig := filepath.Join(tempDir, "atmos.yaml") + err := os.WriteFile(baseConfig, []byte(`auth: + realm: test +`), 0o600) + require.NoError(t, err) + + // Profile "devops" contains auth providers and identities. + profileDir := filepath.Join(tempDir, "profiles", "devops") + require.NoError(t, os.MkdirAll(profileDir, 0o755)) + profileConfig := filepath.Join(profileDir, "atmos.yaml") + err = os.WriteFile(profileConfig, []byte(`auth: + providers: + my-sso: + kind: aws/iam-identity-center + region: us-east-1 + start_url: https://example.awsapps.com/start + identities: + dev-role: + kind: aws/permission-set + provider: my-sso + permission_set: DevOps +`), 0o600) + require.NoError(t, err) + + t.Chdir(tempDir) + + _ = NewTestKit(t) + + cmd := createTestAuthListCmd() + cmd.RunE = executeAuthListCommand + + // Set profile via Viper (matching how global flag binding works in real execution). + // In production, RootCmd's persistent --profile flag is bound to Viper via BindToViper. + // loadAuthManagerForList calls flags.ParseGlobalFlags which reads v.GetStringSlice("profile"). + v := viper.GetViper() + v.Set("profile", []string{"devops"}) + t.Cleanup(func() { v.Set("profile", []string{}) }) + + // Execute — should NOT return ErrAuthNotConfigured because the profile + // contributes auth providers/identities to the merged configuration. + err = cmd.Execute() + // The command should succeed (no ErrAuthNotConfigured) or fail for a different + // reason (e.g., mock provider not available). Either way, it must not be + // ErrAuthNotConfigured which would mean --profile was ignored. + if err != nil { + assert.NotErrorIs(t, err, errUtils.ErrAuthNotConfigured, + "--profile should load auth config and suppress ErrAuthNotConfigured") + } +} + func TestIdentitiesFlagCompletion_ReturnsSortedIdentities(t *testing.T) { // Setup: Create a test config file with identities in non-alphabetical order. tempDir := t.TempDir() diff --git a/internal/exec/utils_auth.go b/internal/exec/utils_auth.go index e7a7d29112..2727c3f9e2 100644 --- a/internal/exec/utils_auth.go +++ b/internal/exec/utils_auth.go @@ -188,7 +188,11 @@ func buildGlobalAuthSection(atmosConfig *schema.AtmosConfiguration) map[string]a if atmosConfig.Auth.Keyring.Type != "" { globalAuthSection["keyring"] = atmosConfig.Auth.Keyring } - if atmosConfig.Auth.Realm != "" { + // Only include realm when explicitly configured (env var or atmos.yaml). + // Auto-computed realms (from config-path hash or default) are path-dependent + // and should not appear in component describe output. + if atmosConfig.Auth.Realm != "" && + (atmosConfig.Auth.RealmSource == "env" || atmosConfig.Auth.RealmSource == "config") { globalAuthSection["realm"] = atmosConfig.Auth.Realm } diff --git a/internal/exec/utils_auth_test.go b/internal/exec/utils_auth_test.go index a286020127..7334b8dbe4 100644 --- a/internal/exec/utils_auth_test.go +++ b/internal/exec/utils_auth_test.go @@ -149,16 +149,49 @@ func TestBuildGlobalAuthSection(t *testing.T) { }, }, { - name: "realm included when set", + name: "realm included when explicitly configured", config: &schema.AtmosConfiguration{ Auth: schema.AuthConfig{ - Realm: "my-project", + Realm: "my-project", + RealmSource: "config", }, }, expected: map[string]any{ "realm": "my-project", }, }, + { + name: "realm included when set via env", + config: &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "env-realm", + RealmSource: "env", + }, + }, + expected: map[string]any{ + "realm": "env-realm", + }, + }, + { + name: "realm excluded when auto-computed from config-path", + config: &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "b80ea18be93f8201", + RealmSource: "config-path", + }, + }, + expected: map[string]any{}, + }, + { + name: "realm excluded when default", + config: &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "default", + RealmSource: "default", + }, + }, + expected: map[string]any{}, + }, { name: "realm excluded when empty", config: &schema.AtmosConfiguration{ @@ -169,10 +202,11 @@ func TestBuildGlobalAuthSection(t *testing.T) { expected: map[string]any{}, }, { - name: "all sections including realm", + name: "all sections including explicit realm", config: &schema.AtmosConfiguration{ Auth: schema.AuthConfig{ - Realm: "prod-realm", + Realm: "prod-realm", + RealmSource: "config", Providers: map[string]schema.Provider{ "aws": {Kind: "aws-iam"}, }, @@ -822,10 +856,11 @@ func TestGetMergedAuthConfigWithFetcher_RealmPropagatedWithEmptyStack(t *testing } func TestMergeGlobalAuthConfig_RealmPropagated(t *testing.T) { - // Verify realm is included in the merged auth section map. + // Verify explicitly configured realm is included in the merged auth section map. atmosConfig := &schema.AtmosConfiguration{ Auth: schema.AuthConfig{ - Realm: "my-project", + Realm: "my-project", + RealmSource: "config", Providers: map[string]schema.Provider{ "aws": {Kind: "aws-iam"}, }, @@ -855,6 +890,24 @@ func TestMergeGlobalAuthConfig_NoRealmConfigured(t *testing.T) { assert.Contains(t, result, "providers") } +func TestMergeGlobalAuthConfig_AutoRealmExcluded(t *testing.T) { + // Auto-computed realm (from config-path hash) should not appear in merged output. + atmosConfig := &schema.AtmosConfiguration{ + Auth: schema.AuthConfig{ + Realm: "b80ea18be93f8201", + RealmSource: "config-path", + Providers: map[string]schema.Provider{ + "aws": {Kind: "aws-iam"}, + }, + }, + } + componentSection := map[string]any{} + + result := mergeGlobalAuthConfig(atmosConfig, componentSection) + assert.NotContains(t, result, "realm") + assert.Contains(t, result, "providers") +} + func TestGetMergedAuthConfigWithFetcher_NoRealmPreservesEmptyRealm(t *testing.T) { // When no realm is configured, the merged config should have empty realm — same as before the fix. atmosConfig := &schema.AtmosConfiguration{ diff --git a/pkg/auth/factory/factory_test.go b/pkg/auth/factory/factory_test.go index c8fdb056e5..6f313c43cc 100644 --- a/pkg/auth/factory/factory_test.go +++ b/pkg/auth/factory/factory_test.go @@ -277,7 +277,7 @@ func TestNewIdentity_Factory(t *testing.T) { if tt.errorType != nil { assert.ErrorIs(t, err, tt.errorType) } - if tt.errorMsg != "" { + if tt.errorMsg != "" && err != nil { assert.Contains(t, err.Error(), tt.errorMsg) } assert.Nil(t, identity) diff --git a/pkg/auth/manager_test.go b/pkg/auth/manager_test.go index d5c384f65b..55d97b5408 100644 --- a/pkg/auth/manager_test.go +++ b/pkg/auth/manager_test.go @@ -2152,85 +2152,75 @@ func TestManager_ResolveProviderConfig(t *testing.T) { } } -func TestInitializeIdentities_EmptyKindWithProfile(t *testing.T) { - m := &manager{ - config: &schema.AuthConfig{ - Identities: map[string]schema.Identity{ - "core-root/terraform": {Kind: ""}, // Empty kind - not configured in profile. +func TestInitializeIdentities(t *testing.T) { + tests := []struct { + name string + identities map[string]schema.Identity + stackInfo *schema.ConfigAndStacksInfo + expectError bool + expectedSentinel error + expectedDetails []string + expectedCount int + }{ + { + name: "empty kind with profile suggests profile mismatch", + identities: map[string]schema.Identity{"core-root/terraform": {Kind: ""}}, + stackInfo: &schema.ConfigAndStacksInfo{ + ProfilesFromArg: []string{"marketplace"}, }, + expectError: true, + expectedSentinel: errUtils.ErrInvalidIdentityConfig, + expectedDetails: []string{"core-root/terraform", "is not configured in the", "marketplace"}, }, - identities: make(map[string]types.Identity), - stackInfo: &schema.ConfigAndStacksInfo{ - ProfilesFromArg: []string{"marketplace"}, + { + name: "empty kind without profile suggests specifying one", + identities: map[string]schema.Identity{"core-root/terraform": {Kind: ""}}, + stackInfo: &schema.ConfigAndStacksInfo{}, + expectError: true, + expectedSentinel: errUtils.ErrInvalidIdentityConfig, + expectedDetails: []string{"core-root/terraform", "is not configured", "Did you forget to specify a profile?"}, }, - } - - err := m.initializeIdentities() - assert.Error(t, err) - assert.ErrorIs(t, err, errUtils.ErrInvalidIdentityConfig) - - // Explanation is stored as a detail on the structured error. - details := cockroachErrors.GetAllDetails(err) - require.NotEmpty(t, details) - assert.Contains(t, details[0], "core-root/terraform") - assert.Contains(t, details[0], "is not configured in the") - assert.Contains(t, details[0], "marketplace") -} - -func TestInitializeIdentities_EmptyKindWithoutProfile(t *testing.T) { - m := &manager{ - config: &schema.AuthConfig{ - Identities: map[string]schema.Identity{ - "core-root/terraform": {Kind: ""}, // Empty kind - not configured. - }, + { + name: "empty kind with nil stackInfo suggests specifying profile", + identities: map[string]schema.Identity{"core-root/terraform": {Kind: ""}}, + stackInfo: nil, + expectError: true, + expectedSentinel: errUtils.ErrInvalidIdentityConfig, + expectedDetails: []string{"Did you forget to specify a profile?"}, }, - identities: make(map[string]types.Identity), - stackInfo: &schema.ConfigAndStacksInfo{}, - } - - err := m.initializeIdentities() - assert.Error(t, err) - assert.ErrorIs(t, err, errUtils.ErrInvalidIdentityConfig) - - details := cockroachErrors.GetAllDetails(err) - require.NotEmpty(t, details) - assert.Contains(t, details[0], "core-root/terraform") - assert.Contains(t, details[0], "is not configured") - assert.Contains(t, details[0], "Did you forget to specify a profile?") -} - -func TestInitializeIdentities_EmptyKindNilStackInfo(t *testing.T) { - m := &manager{ - config: &schema.AuthConfig{ - Identities: map[string]schema.Identity{ - "core-root/terraform": {Kind: ""}, - }, + { + name: "valid kind succeeds", + identities: map[string]schema.Identity{"mock-identity": {Kind: "mock"}}, + stackInfo: &schema.ConfigAndStacksInfo{}, + expectError: false, + expectedCount: 1, }, - identities: make(map[string]types.Identity), - stackInfo: nil, } - err := m.initializeIdentities() - assert.Error(t, err) - assert.ErrorIs(t, err, errUtils.ErrInvalidIdentityConfig) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &manager{ + config: &schema.AuthConfig{ + Identities: tt.identities, + }, + identities: make(map[string]types.Identity), + stackInfo: tt.stackInfo, + } - details := cockroachErrors.GetAllDetails(err) - require.NotEmpty(t, details) - assert.Contains(t, details[0], "Did you forget to specify a profile?") -} + err := m.initializeIdentities() -func TestInitializeIdentities_ValidKindSucceeds(t *testing.T) { - m := &manager{ - config: &schema.AuthConfig{ - Identities: map[string]schema.Identity{ - "mock-identity": {Kind: "mock"}, - }, - }, - identities: make(map[string]types.Identity), - stackInfo: &schema.ConfigAndStacksInfo{}, + if tt.expectError { + assert.Error(t, err) + assert.ErrorIs(t, err, tt.expectedSentinel) + details := cockroachErrors.GetAllDetails(err) + require.NotEmpty(t, details) + for _, expected := range tt.expectedDetails { + assert.Contains(t, details[0], expected) + } + } else { + assert.NoError(t, err) + assert.Len(t, m.identities, tt.expectedCount) + } + }) } - - err := m.initializeIdentities() - assert.NoError(t, err) - assert.Len(t, m.identities, 1) } diff --git a/pkg/auth/realm_mismatch.go b/pkg/auth/realm_mismatch.go index 7c43fe3c49..c57fcd403d 100644 --- a/pkg/auth/realm_mismatch.go +++ b/pkg/auth/realm_mismatch.go @@ -1,6 +1,7 @@ package auth import ( + "errors" "fmt" "os" "path/filepath" @@ -155,6 +156,9 @@ func (m *manager) deleteLegacyCredentialFiles() { awsDir := filepath.Join(baseDir, awsDirNameForMismatch) providerDirs, err := os.ReadDir(awsDir) if err != nil { + if !errors.Is(err, os.ErrNotExist) { + log.Warn("Failed to read legacy credential directory", "path", awsDir, "error", err) + } return } @@ -162,19 +166,30 @@ func (m *manager) deleteLegacyCredentialFiles() { if !provider.IsDir() { continue } - providerDir := filepath.Join(awsDir, provider.Name()) - // Remove credential, config, and lock files from legacy (no-realm) path. - for _, filename := range []string{"credentials", "config", "credentials.lock", "config.lock"} { - filePath := filepath.Join(providerDir, filename) - if err := os.Remove(filePath); err == nil { - log.Debug("Deleted legacy credential file (pre-realm)", "path", filePath) - } - } - // Remove provider directory if now empty. - _ = os.Remove(providerDir) + cleanLegacyProviderDir(filepath.Join(awsDir, provider.Name())) } // Remove aws directory if now empty. - _ = os.Remove(awsDir) + removeIfEmpty(awsDir) +} + +// cleanLegacyProviderDir removes credential, config, and lock files from a legacy provider directory. +func cleanLegacyProviderDir(providerDir string) { + for _, filename := range []string{"credentials", "config", "credentials.lock", "config.lock"} { + filePath := filepath.Join(providerDir, filename) + if err := os.Remove(filePath); err == nil { + log.Debug("Deleted legacy credential file (pre-realm)", "path", filePath) + } else if !errors.Is(err, os.ErrNotExist) { + log.Warn("Failed to delete legacy credential file", "path", filePath, "error", err) + } + } + removeIfEmpty(providerDir) +} + +// removeIfEmpty removes a directory if it is empty, logging unexpected errors. +func removeIfEmpty(dir string) { + if err := os.Remove(dir); err != nil && !errors.Is(err, os.ErrNotExist) { + log.Debug("Could not remove legacy directory (may not be empty)", "path", dir, "error", err) + } } // logRealmMismatchWarning emits a warning about credentials existing under a different realm. diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_dev_(stack-names_example).stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_dev_(stack-names_example).stdout.golden index 9e2b28ab8c..fb0ad9e84b 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_dev_(stack-names_example).stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_dev_(stack-names_example).stdout.golden @@ -75,9 +75,7 @@ "atmos_manifest": "dev", "atmos_stack": "dev", "atmos_stack_file": "dev", - "auth": { - "realm": "178007337c087aed" - }, + "auth": {}, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_production_(stack-names_example).stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_production_(stack-names_example).stdout.golden index 5b485bec0d..c5db9c607b 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_production_(stack-names_example).stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_mock_-s_production_(stack-names_example).stdout.golden @@ -75,9 +75,7 @@ "atmos_manifest": "prod", "atmos_stack": "production", "atmos_stack_file": "prod", - "auth": { - "realm": "178007337c087aed" - }, + "auth": {}, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_dev_(native-terraform_example).stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_dev_(native-terraform_example).stdout.golden index 0e025e191e..6efd0b7941 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_dev_(native-terraform_example).stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_dev_(native-terraform_example).stdout.golden @@ -77,9 +77,7 @@ "atmos_manifest": "dev", "atmos_stack": "dev", "atmos_stack_file": "dev", - "auth": { - "realm": "7bb257e4fdba764d" - }, + "auth": {}, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_my-legacy-prod-stack.stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_my-legacy-prod-stack.stdout.golden index 6e863507aa..31bcb1c3c5 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_my-legacy-prod-stack.stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_my-legacy-prod-stack.stdout.golden @@ -77,9 +77,7 @@ "atmos_manifest": "legacy-prod", "atmos_stack": "my-legacy-prod-stack", "atmos_stack_file": "legacy-prod", - "auth": { - "realm": "04bfae195bf1970c" - }, + "auth": {}, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_no-name-prod.stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_no-name-prod.stdout.golden index 87469f96dc..1837d11468 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_no-name-prod.stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_no-name-prod.stdout.golden @@ -77,9 +77,7 @@ "atmos_manifest": "no-name-prod", "atmos_stack": "no-name-prod", "atmos_stack_file": "no-name-prod", - "auth": { - "realm": "04bfae195bf1970c" - }, + "auth": {}, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_production_(native-terraform_example).stdout.golden b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_production_(native-terraform_example).stdout.golden index 1a97892d29..97710fe8f5 100644 --- a/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_production_(native-terraform_example).stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_describe_component_vpc_-s_production_(native-terraform_example).stdout.golden @@ -77,9 +77,7 @@ "atmos_manifest": "prod", "atmos_stack": "production", "atmos_stack_file": "prod", - "auth": { - "realm": "7bb257e4fdba764d" - }, + "auth": {}, "backend": {}, "backend_type": "", "cli_args": [ diff --git a/tests/snapshots/TestCLICommands_describe_component_from_nested_dir_discovers_atmos.yaml_in_parent.stdout.golden b/tests/snapshots/TestCLICommands_describe_component_from_nested_dir_discovers_atmos.yaml_in_parent.stdout.golden index 5846979970..7053583b5f 100644 --- a/tests/snapshots/TestCLICommands_describe_component_from_nested_dir_discovers_atmos.yaml_in_parent.stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_from_nested_dir_discovers_atmos.yaml_in_parent.stdout.golden @@ -51,8 +51,7 @@ atmos_component: test-component atmos_manifest: test-stack atmos_stack: test atmos_stack_file: test-stack -auth: - realm: a14b6bb632762f7d +auth: {} backend: {} backend_type: "" cli_args: diff --git a/tests/snapshots/TestCLICommands_describe_component_with_component_name_(backward_compatibility).stdout.golden b/tests/snapshots/TestCLICommands_describe_component_with_component_name_(backward_compatibility).stdout.golden index 5b902298ef..695a23a87e 100644 --- a/tests/snapshots/TestCLICommands_describe_component_with_component_name_(backward_compatibility).stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_with_component_name_(backward_compatibility).stdout.golden @@ -53,8 +53,7 @@ atmos_component: top-level-component1 atmos_manifest: orgs/cp/tenant1/dev/us-east-2 atmos_stack: tenant1-ue2-dev atmos_stack_file: orgs/cp/tenant1/dev/us-east-2 -auth: - realm: b80ea18be93f8201 +auth: {} backend: acl: bucket-owner-full-control bucket: cp-ue2-root-tfstate diff --git a/tests/snapshots/TestCLICommands_describe_component_with_current_directory_(.).stdout.golden b/tests/snapshots/TestCLICommands_describe_component_with_current_directory_(.).stdout.golden index 5aba72fee2..4798a136ca 100644 --- a/tests/snapshots/TestCLICommands_describe_component_with_current_directory_(.).stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_with_current_directory_(.).stdout.golden @@ -53,8 +53,7 @@ atmos_component: top-level-component1 atmos_manifest: orgs/cp/tenant1/dev/us-east-2 atmos_stack: tenant1-ue2-dev atmos_stack_file: orgs/cp/tenant1/dev/us-east-2 -auth: - realm: b80ea18be93f8201 +auth: {} backend: acl: bucket-owner-full-control bucket: cp-ue2-root-tfstate diff --git a/tests/snapshots/TestCLICommands_describe_component_with_relative_path.stdout.golden b/tests/snapshots/TestCLICommands_describe_component_with_relative_path.stdout.golden index 5aba72fee2..4798a136ca 100644 --- a/tests/snapshots/TestCLICommands_describe_component_with_relative_path.stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_with_relative_path.stdout.golden @@ -53,8 +53,7 @@ atmos_component: top-level-component1 atmos_manifest: orgs/cp/tenant1/dev/us-east-2 atmos_stack: tenant1-ue2-dev atmos_stack_file: orgs/cp/tenant1/dev/us-east-2 -auth: - realm: b80ea18be93f8201 +auth: {} backend: acl: bucket-owner-full-control bucket: cp-ue2-root-tfstate diff --git a/tests/snapshots/TestCLICommands_describe_component_with_stack_flag.stdout.golden b/tests/snapshots/TestCLICommands_describe_component_with_stack_flag.stdout.golden index bd236dfe54..6d28e681f2 100644 --- a/tests/snapshots/TestCLICommands_describe_component_with_stack_flag.stdout.golden +++ b/tests/snapshots/TestCLICommands_describe_component_with_stack_flag.stdout.golden @@ -52,8 +52,7 @@ atmos_component: vpc atmos_manifest: orgs/acme/plat/dev/us-east-2 atmos_stack: plat-ue2-dev atmos_stack_file: orgs/acme/plat/dev/us-east-2 -auth: - realm: b14b9545124ec327 +auth: {} backend: {} backend_type: "" cli_args: