Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
76b0d1d
fix: Consolidate credential retrieval logic to fix terraform auth
osterman Oct 28, 2025
1f3fab3
fix: Improve mock identity to match real AWS identity behavior
osterman Oct 28, 2025
d288a76
fix: Add sanitization for 'Last Updated' timestamps in test snapshots
osterman Oct 28, 2025
9e3c054
refactor: Implement stateful mock identity for provider-agnostic testing
osterman Oct 28, 2025
5f1e01d
fix: Address PR feedback - improve error handling and documentation
osterman Oct 28, 2025
a813f42
refactor: Remove AWS-specific logging and paths from auth manager
osterman Oct 28, 2025
19e37b3
fix: Use file-based persistence in mock identity for cross-process cr…
osterman Oct 28, 2025
9f33840
fix: Remove ANSI code leak in auth logout success messages
osterman Oct 28, 2025
06c9239
feat: Add ATMOS_IDENTITY environment variable support for terraform c…
osterman Oct 28, 2025
bd81670
test: Add tests for ATMOS_IDENTITY environment variable support
osterman Oct 28, 2025
8103ee0
fix: Prevent identity flag from overwriting ATMOS_IDENTITY environmen…
osterman Oct 28, 2025
e25621c
feat: Add --all flag to auth logout command
osterman Oct 28, 2025
fbe89ee
docs: Add --all flag documentation to auth logout command
osterman Oct 28, 2025
07453cc
debug: Add detailed logging for credential storage and retrieval
osterman Oct 28, 2025
41517db
refactor: Use errors.Is for idiomatic error checking in mock identity…
osterman Oct 28, 2025
80983e8
debug: Add logging for credentials written to files
osterman Oct 28, 2025
cdca4c2
fix: Use root provider name for file storage instead of via identity
osterman Oct 28, 2025
2b6aef5
debug: Add detailed logging for AWS file cleanup operations
osterman Oct 28, 2025
3f37f44
Merge branch 'main' into fix-terraform-auth-context
aknysh Oct 28, 2025
2f1c0a8
fix: Convert ComponentEnvSection to ComponentEnvList and fix log leve…
osterman Oct 28, 2025
aefa9ba
fix: Reduce log verbosity and fix doc comment
osterman Oct 28, 2025
d709868
feat: implement selective identity logout and add auth status indicators
osterman Oct 29, 2025
d45ff6f
fix: add period to godot linter comment
osterman Oct 29, 2025
b1f34d8
fix: show legacy path warning only once per execution
osterman Oct 29, 2025
e7b134b
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 29, 2025
b5d99f3
feat: Add case-insensitive identity name resolution
osterman Oct 29, 2025
771c821
feat: Add interactive identity selection to terraform commands
osterman Oct 29, 2025
0abffa6
refactor: Extract component env conversion to shared function
osterman Oct 29, 2025
43022ba
docs: Add comprehensive godoc to LevelToString and add perf tracking
osterman Oct 29, 2025
9553f6e
docs: Fix verb form from 'logout' to 'log out' in documentation
osterman Oct 29, 2025
becf7e8
docs: Update PRD to reflect implemented credential retrieval consolid…
osterman Oct 29, 2025
16cd628
refactor: Replace viper binding with direct os.Getenv for ATMOS_IDENTITY
osterman Oct 29, 2025
821cd48
docs: Add blog post about authentication UX improvements
osterman Oct 29, 2025
415d17a
docs: Rename blog post to .mdx and update CLAUDE.md requirements
osterman Oct 29, 2025
9b0f4e1
docs: Clarify logout behavior in blog post
osterman Oct 29, 2025
c1cea3d
fix: LogoutAll now logs out providers in addition to identities
osterman Oct 29, 2025
bb00279
docs: Update blog post to reflect --all now logs out providers
osterman Oct 29, 2025
68ca486
test: Add test to verify LogoutAll logs out providers
osterman Oct 29, 2025
710d960
docs: Document LogoutAll bug fix in blog post
osterman Oct 29, 2025
ce8edf1
test: Update auth list snapshot with status indicators
osterman Oct 29, 2025
c998aae
fix: Add defensive nil check for authManager in buildIdentityTitle
osterman Oct 29, 2025
7786c8f
docs: Improve credential expiration log messages for clarity
osterman Oct 29, 2025
7f9198f
test: Add sanitization for credential expiration durations in snapshots
osterman Oct 29, 2025
6ee031d
test: Update nonexistent identity test expectations
osterman Oct 29, 2025
20c1e91
fix: Correct duration sanitization to use standard '1h 0m' format
osterman Oct 29, 2025
c014d28
test: Add sanitization for platform-specific credential_store values
osterman Oct 29, 2025
fe842d0
refactor: Use 'keyring' instead of angle-bracket placeholder
osterman Oct 29, 2025
50fdb85
refactor: Use 'keyring-placeholder' for credential_store sanitization
osterman Oct 29, 2025
23997c8
style: Add missing period to inline comment in load.go
osterman Oct 29, 2025
c1db0b2
refactor: Add TTY guard and optimize config init for interactive iden…
osterman Oct 29, 2025
3104560
docs: Address CodeRabbit feedback for documentation and tests
osterman Oct 29, 2025
8f340cc
fix: Make duration sanitization regex handle hours-only format
osterman Oct 29, 2025
a1e4ef1
fix: Correct broken link in blog post to use /cli/commands/auth/usage
osterman Oct 29, 2025
e68c68e
docs: Add documentation link verification guidelines to CLAUDE.md
osterman Oct 29, 2025
167839e
docs: Fix inconsistent function name in PRD documentation
osterman Oct 29, 2025
6e1ef3d
fix: Check both stdin and stdout TTY support for interactive identity…
osterman Oct 29, 2025
3e12eb0
fix: Strip trailing whitespace in snapshots when ignore_trailing_whit…
osterman Oct 29, 2025
8d4c40a
docs: Update CLAUDE.md documentation link examples with correct patterns
osterman Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,43 @@ See `docs/developing-atmos-commands.md` and `docs/prd/command-registry-pattern.m
### Documentation (MANDATORY)
All cmds/flags need Docusaurus docs in `website/docs/cli/commands/`. Use `<dl>` for args/flags. Build: `cd website && npm run build`

**Verifying Documentation Links (MANDATORY):**
Before adding links to documentation pages, ALWAYS verify the correct URL:

```bash
# Example: Finding the correct URL for auth user configure command
# Step 1: Find the doc file
find website/docs/cli/commands -name "*user-configure*"
# Output: website/docs/cli/commands/auth/auth-user-configure.mdx

# Step 2: Check the slug in frontmatter
head -10 website/docs/cli/commands/auth/auth-user-configure.mdx | grep slug
# Output: slug: /cli/commands/auth/auth-user-configure

# Step 3: Verify by checking existing links
grep -r "/cli/commands/auth/auth-user-configure" website/docs/
```

**Common mistakes:**
- Using command name instead of filename (e.g., `/cli/commands/auth/atmos_auth` when file is `usage.mdx`)
- Not checking the `slug` frontmatter which can override default URLs
- Guessing URLs instead of verifying against existing documentation structure

**Correct approach:**
1. Find the target doc file: `find website/docs/cli/commands -name "*keyword*"`
2. Check for `slug:` in frontmatter: `head -10 <file> | grep slug`
3. If no slug, URL is path from `docs/` without extension (e.g., `auth-user-configure.mdx` → `/cli/commands/auth/auth-user-configure`)
4. Verify by searching for existing links: `grep -r "<url>" website/docs/`

### PRD Documentation (MANDATORY)
All Product Requirement Documents (PRDs) MUST be placed in `docs/prd/`. Use kebab-case filenames. Examples: `command-registry-pattern.md`, `error-handling-strategy.md`, `testing-strategy.md`

### Pull Requests (MANDATORY)
Follow template (what/why/references).

**Blog Posts (CI Enforced):**
- PRs labeled `minor` or `major` MUST include blog post in `website/blog/YYYY-MM-DD-feature-name.md`
- PRs labeled `minor` or `major` MUST include blog post in `website/blog/YYYY-MM-DD-feature-name.mdx`
- Blog posts must use `.mdx` extension with YAML front matter
- Include `<!--truncate-->` after intro paragraph
- Tag `feature`/`enhancement`/`bugfix` (user-facing) or `contributors` (internal changes)
- CI will fail without blog post
Expand Down
13 changes: 11 additions & 2 deletions cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
)

const (
IdentityFlagName = "identity"
IdentityFlagName = "identity"
IdentityFlagSelectValue = "__SELECT__" // Special value when --identity is used without argument.
)

// authCmd groups authentication-related subcommands.
Expand All @@ -23,7 +24,15 @@ var authCmd = &cobra.Command{
func init() {
// Avoid adding "stack" at the group level unless subcommands require it.
// AddStackCompletion(authCmd)
authCmd.PersistentFlags().StringP(IdentityFlagName, "i", "", "Specify the target identity to assume.")
authCmd.PersistentFlags().StringP(IdentityFlagName, "i", "", "Specify the target identity to assume. Use without value to interactively select.")

// Set NoOptDefVal to enable optional flag value.
// When --identity is used without a value, it will receive IdentityFlagSelectValue.
identityFlag := authCmd.PersistentFlags().Lookup(IdentityFlagName)
if identityFlag != nil {
identityFlag.NoOptDefVal = IdentityFlagSelectValue
}

// Bind to Viper and env (flags > env > config > defaults).
if err := viper.BindEnv(IdentityFlagName, "ATMOS_IDENTITY", "IDENTITY"); err != nil {
log.Trace("Failed to bind identity environment variables", "error", err)
Expand Down
8 changes: 6 additions & 2 deletions cmd/auth_console.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,15 @@ func resolveIdentityName(cmd *cobra.Command, authManager types.AuthManager) (str
defer perf.Track(nil, "cmd.resolveIdentityName")()

identityName, _ := cmd.Flags().GetString(IdentityFlagName)
if identityName != "" {

// Check if user wants to interactively select identity.
forceSelect := identityName == IdentityFlagSelectValue

if identityName != "" && !forceSelect {
return identityName, nil
}

identityName, err := authManager.GetDefaultIdentity()
identityName, err := authManager.GetDefaultIdentity(forceSelect)
if err != nil {
return "", fmt.Errorf("%w: failed to get default identity: %w", errUtils.ErrAuthConsole, err)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/auth_console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ func (m *mockAuthManagerForProvider) Authenticate(ctx context.Context, identityN
return nil, errors.New("not implemented")
}

func (m *mockAuthManagerForProvider) GetDefaultIdentity() (string, error) {
func (m *mockAuthManagerForProvider) GetDefaultIdentity(_ bool) (string, error) {
return "", errors.New("not implemented")
}

Expand Down Expand Up @@ -675,7 +675,7 @@ func (m *mockAuthManagerForIdentity) Authenticate(ctx context.Context, identityN
return nil, errors.New("not implemented")
}

func (m *mockAuthManagerForIdentity) GetDefaultIdentity() (string, error) {
func (m *mockAuthManagerForIdentity) GetDefaultIdentity(_ bool) (string, error) {
if m.defaultErr != nil {
return "", m.defaultErr
}
Expand Down
8 changes: 6 additions & 2 deletions cmd/auth_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ var authEnvCmd = &cobra.Command{

// Get identity from flag or use default
identityName, _ := cmd.Flags().GetString("identity")
if identityName == "" {
defaultIdentity, err := authManager.GetDefaultIdentity()

// Check if user wants to interactively select identity.
forceSelect := identityName == IdentityFlagSelectValue

if identityName == "" || forceSelect {
defaultIdentity, err := authManager.GetDefaultIdentity(forceSelect)
if err != nil {
return fmt.Errorf("no default identity configured and no identity specified: %w", err)
}
Expand Down
8 changes: 6 additions & 2 deletions cmd/auth_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@ func executeAuthExecCommandCore(cmd *cobra.Command, args []string) error {

// Get identity from flag or use default
identityName, _ := cmd.Flags().GetString("identity")
if identityName == "" {
defaultIdentity, err := authManager.GetDefaultIdentity()

// Check if user wants to interactively select identity.
forceSelect := identityName == IdentityFlagSelectValue

if identityName == "" || forceSelect {
defaultIdentity, err := authManager.GetDefaultIdentity(forceSelect)
if err != nil {
return errors.Join(errUtils.ErrNoDefaultIdentity, err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/auth_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestAuthCLIIntegrationWithCloudProvider(t *testing.T) {
assert.NotNil(t, authManager)

// Test GetDefaultIdentity
defaultIdentity, err := authManager.GetDefaultIdentity()
defaultIdentity, err := authManager.GetDefaultIdentity(false)
require.NoError(t, err)
assert.Equal(t, "test-identity", defaultIdentity)

Expand Down
8 changes: 6 additions & 2 deletions cmd/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,13 @@ func executeAuthLoginCommand(cmd *cobra.Command, args []string) error {
identityName = viper.GetString(IdentityFlagName)
}

// Check if user wants to interactively select identity.
forceSelect := identityName == IdentityFlagSelectValue

// If no identity specified, get the default identity (which prompts if needed).
if identityName == "" {
identityName, err = authManager.GetDefaultIdentity()
// If --identity flag was used without value, forceSelect will be true.
if identityName == "" || forceSelect {
identityName, err = authManager.GetDefaultIdentity(forceSelect)
if err != nil {
return errors.Join(errUtils.ErrDefaultIdentity, err)
}
Expand Down
23 changes: 16 additions & 7 deletions cmd/auth_logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ This command removes:
• AWS credential files (~/.aws/atmos/<provider>/credentials)
• AWS config files (~/.aws/atmos/<provider>/config)

You can specify the identity to logout using either:
• Positional argument: atmos auth logout <identity>
• Flag: atmos auth logout --identity <identity>
You can specify what to logout:
• Specific identity: atmos auth logout <identity>
• All identities: atmos auth logout --all
• Specific provider: atmos auth logout --provider <provider>
• Interactive mode: atmos auth logout (no arguments)

Note: This only removes local credentials. Your browser session with the
identity provider (AWS SSO, Okta, etc.) may still be active. To completely
Expand Down Expand Up @@ -62,6 +64,7 @@ func executeAuthLogoutCommand(cmd *cobra.Command, args []string) error {

// Get flags.
providerFlag, _ := cmd.Flags().GetString("provider")
allFlag, _ := cmd.Flags().GetBool("all")
dryRun, _ := cmd.Flags().GetBool("dry-run")

// Get identity from flag or positional argument.
Expand All @@ -74,6 +77,11 @@ func executeAuthLogoutCommand(cmd *cobra.Command, args []string) error {
ctx := context.Background()

// Determine what to logout.
if allFlag {
// Logout all identities.
return performLogoutAll(ctx, authManager, dryRun)
}

if providerFlag != "" {
// Logout specific provider.
return performProviderLogout(ctx, authManager, providerFlag, dryRun)
Expand Down Expand Up @@ -132,16 +140,16 @@ func performIdentityLogout(ctx context.Context, authManager auth.AuthManager, id
if err := authManager.Logout(ctx, identityName); err != nil {
// Check if it's a partial logout.
if errors.Is(err, errUtils.ErrPartialLogout) {
u.PrintfMarkdownToTUI("\n%s Logged out **%s** with warnings\n\n", theme.Styles.Checkmark, identityName)
u.PrintfMessageToTUI("\n%s Logged out %s with warnings\n\n", theme.Styles.Checkmark, identityName)
u.PrintfMessageToTUI("Some credentials could not be removed:\n")
u.PrintfMessageToTUI(" %v\n\n", err)
} else {
u.PrintfMarkdownToTUI("\n%s Failed to log out **%s**\n\n", theme.Styles.XMark, identityName)
u.PrintfMessageToTUI("\n%s Failed to log out %s\n\n", theme.Styles.XMark, identityName)
u.PrintfMessageToTUI("Error: %v\n\n", err)
return err
}
} else {
u.PrintfMarkdownToTUI("\n%s Logged out **%s**\n\n", theme.Styles.Checkmark, identityName)
u.PrintfMessageToTUI("\n%s Logged out %s\n\n", theme.Styles.Checkmark, identityName)
}

// Display browser session warning.
Expand Down Expand Up @@ -200,7 +208,7 @@ func performProviderLogout(ctx context.Context, authManager auth.AuthManager, pr
return err
}

u.PrintfMarkdownToTUI("\n%s Logged out provider **%s** (%d identities)\n\n", theme.Styles.Checkmark, providerName, len(identitiesForProvider))
u.PrintfMessageToTUI("\n%s Logged out provider %s (%d identities)\n\n", theme.Styles.Checkmark, providerName, len(identitiesForProvider))

// Display browser session warning.
displayBrowserWarning()
Expand Down Expand Up @@ -350,6 +358,7 @@ func displayBrowserWarning() {

func init() {
authLogoutCmd.Flags().String("provider", "", "Logout from specific provider")
authLogoutCmd.Flags().Bool("all", false, "Logout from all identities and providers")
authLogoutCmd.Flags().Bool("dry-run", false, "Preview what would be removed without deleting")
authCmd.AddCommand(authLogoutCmd)
}
69 changes: 69 additions & 0 deletions cmd/auth_logout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,72 @@ func TestExecuteAuthLogoutCommand_SupportsIdentityFlag(t *testing.T) {
})
}
}

func TestPerformLogoutAll_WithAllFlag(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

tests := []struct {
name string
dryRun bool
setupMocks func(*types.MockAuthManager)
expectedError error
}{
{
name: "all flag triggers logout all",
dryRun: false,
setupMocks: func(m *types.MockAuthManager) {
m.EXPECT().LogoutAll(gomock.Any()).Return(nil)
m.EXPECT().GetIdentities().Return(map[string]schema.Identity{
"identity1": {Kind: "aws/permission-set"},
"identity2": {Kind: "aws/user"},
})
},
expectedError: nil,
},
{
name: "all flag with dry run",
dryRun: true,
setupMocks: func(m *types.MockAuthManager) {
m.EXPECT().GetProviders().Return(map[string]schema.Provider{
"provider1": {},
})
m.EXPECT().GetFilesDisplayPath("provider1").Return("/home/user/.config/atmos")
},
expectedError: nil,
},
{
name: "all flag with partial logout",
dryRun: false,
setupMocks: func(m *types.MockAuthManager) {
m.EXPECT().LogoutAll(gomock.Any()).Return(errUtils.ErrPartialLogout)
},
expectedError: nil, // Partial logout treated as success.
},
{
name: "all flag with logout failure",
dryRun: false,
setupMocks: func(m *types.MockAuthManager) {
m.EXPECT().LogoutAll(gomock.Any()).Return(errUtils.ErrLogoutFailed)
},
expectedError: errUtils.ErrLogoutFailed,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockManager := types.NewMockAuthManager(ctrl)
tt.setupMocks(mockManager)

ctx := context.Background()
err := performLogoutAll(ctx, mockManager, tt.dryRun)

if tt.expectedError != nil {
assert.Error(t, err)
assert.ErrorIs(t, err, tt.expectedError)
} else {
assert.NoError(t, err)
}
})
}
}
8 changes: 6 additions & 2 deletions cmd/auth_shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ func executeAuthShellCommandCore(cmd *cobra.Command, args []string) error {

// Get identity from viper (respects CLI → ENV → config precedence).
identityName := viper.GetString(IdentityFlagName)
if identityName == "" {
defaultIdentity, err := authManager.GetDefaultIdentity()

// Check if user wants to interactively select identity.
forceSelect := identityName == IdentityFlagSelectValue

if identityName == "" || forceSelect {
defaultIdentity, err := authManager.GetDefaultIdentity(forceSelect)
if err != nil {
return errors.Join(errUtils.ErrNoDefaultIdentity, err)
}
Expand Down
9 changes: 7 additions & 2 deletions cmd/auth_whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,15 @@ func loadAuthManager() (authTypes.AuthManager, error) {

func identityFromFlagOrDefault(cmd *cobra.Command, authManager authTypes.AuthManager) (string, error) {
identityName, _ := cmd.Flags().GetString("identity")
if identityName != "" {

// Check if user wants to interactively select identity.
forceSelect := identityName == IdentityFlagSelectValue

if identityName != "" && !forceSelect {
return identityName, nil
}
defaultIdentity, err := authManager.GetDefaultIdentity()

defaultIdentity, err := authManager.GetDefaultIdentity(forceSelect)
if err != nil {
return "", fmt.Errorf("%w: no default identity configured and no identity specified: %v", errUtils.ErrInvalidAuthConfig, err)
}
Expand Down
8 changes: 7 additions & 1 deletion cmd/terraform_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,13 @@ func attachTerraformCommands(parentCmd *cobra.Command) {
parentCmd.PersistentFlags().Bool("process-templates", true, "Enable/disable Go template processing in Atmos stack manifests when executing terraform commands")
parentCmd.PersistentFlags().Bool("process-functions", true, "Enable/disable YAML functions processing in Atmos stack manifests when executing terraform commands")
parentCmd.PersistentFlags().StringSlice("skip", nil, "Skip executing specific YAML functions in the Atmos stack manifests when executing terraform commands")
parentCmd.PersistentFlags().StringP("identity", "i", "", "Specify the identity to authenticate to before running Terraform commands")
parentCmd.PersistentFlags().StringP("identity", "i", "", "Specify the identity to authenticate to before running Terraform commands. Use without value to interactively select.")

// Set NoOptDefVal to enable optional flag value for identity.
// When --identity is used without a value, it will receive IdentityFlagSelectValue.
if identityFlag := parentCmd.PersistentFlags().Lookup("identity"); identityFlag != nil {
identityFlag.NoOptDefVal = IdentityFlagSelectValue
}

parentCmd.PersistentFlags().StringP("query", "q", "", "Execute `atmos terraform <command>` on the components filtered by a YQ expression, in all stacks or in a specific stack")
parentCmd.PersistentFlags().StringSlice("components", nil, "Filter by specific components")
Expand Down
Loading
Loading