Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9abfcb6
Changes auto-committed by Conductor
osterman Oct 17, 2025
506bda1
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 17, 2025
4b1d882
feat: Make AWS credentials directory configurable via provider spec
osterman Oct 17, 2025
c1ca11e
fix: Display actual configured paths in auth logout dry-run output
osterman Oct 17, 2025
6baf18f
feat: Add validation for spec.files.base_path in AWS providers
osterman Oct 17, 2025
7b048e0
docs: Add spec.files.base_path configuration documentation
osterman Oct 17, 2025
57677f7
Merge branch 'main' into feature/dev-3705-implement-atmos-auth-logout…
osterman Oct 18, 2025
77ea4ca
refactor: Add perf tracking, remove unused error, remove env var support
osterman Oct 18, 2025
75c3c17
feat: Add logout not supported vs not implemented distinction
osterman Oct 18, 2025
c84924d
refactor: Fix N+1 provider cleanup and improve error handling in logout
osterman Oct 18, 2025
127a26d
fix: Add identity-scoped cleanup to preserve other identities' creden…
osterman Oct 18, 2025
43177be
test: Update TestDelete_Flow to expect success for non-existent crede…
osterman Oct 18, 2025
c6095cd
feat: Add basePath parameter to AWS setup functions
osterman Oct 18, 2025
0fa051b
test: Add comprehensive tests for auth logout functionality
osterman Oct 19, 2025
264e826
test: Add Logout tests for AWS and GitHub providers
osterman Oct 19, 2025
70beb13
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 19, 2025
1555073
test: Add comprehensive logout tests for manager and identities
osterman Oct 19, 2025
3c89fa3
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 19, 2025
b51bee3
Merge remote-tracking branch 'origin/main' into feature/dev-3705-impl…
osterman Oct 19, 2025
d5bf86c
Changes auto-committed by Conductor
osterman Oct 19, 2025
608d9ee
Merge branch 'main' into feature/dev-3705-implement-atmos-auth-logout…
osterman Oct 19, 2025
a3308f6
Changes auto-committed by Conductor
osterman Oct 19, 2025
e819fcd
Add performance instrumentation and error classification to AWS provi…
osterman Oct 19, 2025
d36748b
Fix Windows path separator compatibility in GetFilesDisplayPath tests
osterman Oct 19, 2025
d11c3ca
Merge branch 'main' into feature/dev-3705-implement-atmos-auth-logout…
osterman Oct 19, 2025
2ce1d93
Add performance instrumentation to SAML provider Validate method
osterman Oct 19, 2025
fcaf6c3
Changes auto-committed by Conductor
osterman Oct 20, 2025
133c9a1
Ignore merge conflict artifacts in gitignore
osterman Oct 20, 2025
f8c9bf6
Add performance instrumentation to AWS identity Logout methods
osterman Oct 20, 2025
616af40
Add test coverage for auth components
osterman Oct 20, 2025
2ecf77f
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 20, 2025
ac9fe05
Improve logout UI with concise, modern messaging
osterman Oct 20, 2025
cb66d3f
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 20, 2025
6f2e5bd
Use PrintfMarkdownToTUI for all markdown-formatted messages
osterman Oct 20, 2025
f806a12
Show browser session warning only once using cache
osterman Oct 20, 2025
bb5cb3b
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 20, 2025
35db3ae
Fix AWS env isolation to prevent shared config file loading
osterman Oct 20, 2025
44b23bb
Merge remote-tracking branch 'origin/main' into feature/dev-3705-impl…
osterman Oct 20, 2025
7d7422a
Fix auth logout tests to expect GetIdentities and GetProviderForIdent…
osterman Oct 20, 2025
d052c3e
Fix AWS setup tests using homedir.Reset() to clear cache
osterman Oct 20, 2025
547ec56
Merge remote-tracking branch 'origin/main' into feature/dev-3705-impl…
osterman Oct 20, 2025
d365c2c
Changes auto-committed by Conductor
osterman Oct 20, 2025
27cfbd4
Merge remote-tracking branch 'origin/main' into feature/dev-3705-impl…
osterman Oct 20, 2025
f2b9286
Merge branch 'main' into feature/dev-3705-implement-atmos-auth-logout…
osterman Oct 21, 2025
a334d19
Merge remote-tracking branch 'origin/main' into feature/dev-3705-impl…
osterman Oct 21, 2025
70f4ab8
Merge branch 'main' into feature/dev-3705-implement-atmos-auth-logout…
osterman Oct 21, 2025
4a5b74e
Replace deprecated github.com/golang/mock with go.uber.org/mock
osterman Oct 21, 2025
aef4273
Merge remote-tracking branch 'origin/main' into feature/dev-3705-impl…
osterman Oct 21, 2025
0052e7d
Add perf instrumentation and fix import ordering
osterman Oct 21, 2025
98a8666
Add blog post for atmos auth shell with corrected problem statement
osterman Oct 21, 2025
475404d
Fix missing periods in blog post list items
osterman Oct 21, 2025
7e34ee7
Remove auth shell blog post and add auth logout blog post
osterman Oct 21, 2025
3e2443b
Remove duplicate auth logout blog post
osterman Oct 21, 2025
6b651d3
Update auth logout blog post problem statement
osterman Oct 21, 2025
a993889
Update auth logout documentation with improved problem statement
osterman Oct 21, 2025
b4bc983
Move problem statement to dedicated section in logout docs
osterman Oct 21, 2025
514ce9c
Merge branch 'main' into feature/dev-3705-implement-atmos-auth-logout…
osterman Oct 21, 2025
f2c44c2
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 21, 2025
cba7fe8
Fix AWS auth tests to use forked homedir package
osterman Oct 21, 2025
40baedc
Add lint rule to forbid mitchellh/go-homedir imports
osterman Oct 21, 2025
8ac86aa
Fix blog post formatting and homedir test flakiness
osterman Oct 21, 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
300 changes: 300 additions & 0 deletions cmd/auth_logout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
package cmd

import (
"context"
"errors"
"fmt"

"github.com/charmbracelet/huh"
"github.com/spf13/cobra"

errUtils "github.com/cloudposse/atmos/errors"
uiutils "github.com/cloudposse/atmos/internal/tui/utils"
"github.com/cloudposse/atmos/pkg/auth"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
)

// authLogoutCmd logs out by removing local credentials.
var authLogoutCmd = &cobra.Command{
Use: "logout [identity]",
Short: "Remove locally cached credentials and session data",
Long: `Removes cached credentials from the system keyring and local credential files.

This command removes:
• Credentials stored in system keyring
• AWS credential files (~/.aws/atmos/<provider>/credentials)
• AWS config files (~/.aws/atmos/<provider>/config)

Note: This only removes local credentials. Your browser session with the
identity provider (AWS SSO, Okta, etc.) may still be active. To completely
end your session, visit your identity provider's website and sign out.`,

FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
ValidArgsFunction: ComponentsArgCompletion,
RunE: executeAuthLogoutCommand,
}

func executeAuthLogoutCommand(cmd *cobra.Command, args []string) error {
handleHelpRequest(cmd, args)

// Load atmos config.
atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
if err != nil {
return fmt.Errorf("%w: %w", errUtils.ErrFailedToInitializeAtmosConfig, err)
}

// Create auth manager.
authManager, err := createAuthManager(&atmosConfig.Auth)
if err != nil {
return fmt.Errorf("%w: %w", errUtils.ErrAuthManager, err)
}

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

ctx := context.Background()

// Determine what to logout.
if providerFlag != "" {
// Logout specific provider.
return performProviderLogout(ctx, authManager, providerFlag, dryRun)
}

if len(args) > 0 {
// Logout specific identity.
identityName := args[0]
return performIdentityLogout(ctx, authManager, identityName, dryRun)
}

// Interactive mode: prompt user to choose.
return performInteractiveLogout(ctx, authManager, dryRun)
}

// performIdentityLogout removes credentials for a specific identity.
func performIdentityLogout(ctx context.Context, authManager auth.AuthManager, identityName string, dryRun bool) error {
// Validate identity exists.
identities := authManager.GetIdentities()
if _, exists := identities[identityName]; !exists {
u.PrintfMessageToTUI("**Error:** identity %q not found in configuration\n\n", identityName)
u.PrintfMessageToTUI("**Available identities:**\n")
for name := range identities {
u.PrintfMessageToTUI(" • %s\n", name)
}
u.PrintfMessageToTUI("\n")
return fmt.Errorf("%w: identity %q", errUtils.ErrIdentityNotInConfig, identityName)
}

u.PrintfMessageToTUI("\n**Logging out from identity:** %s\n\n", identityName)

// Get authentication chain to show what will be removed.
providerName := authManager.GetProviderForIdentity(identityName)

if dryRun {
u.PrintfMessageToTUI("**Dry run mode:** No credentials will be removed\n\n")
u.PrintfMessageToTUI("**Would remove:**\n")
u.PrintfMessageToTUI(" • Keyring entry: %s\n", identityName)
if providerName != "" {
u.PrintfMessageToTUI(" • Keyring entry: %s (provider)\n", providerName)
// Get actual configured path for this provider.
basePath := authManager.GetFilesDisplayPath(providerName)
u.PrintfMessageToTUI(" • Files: %s/%s/\n", basePath, providerName)
}
u.PrintfMessageToTUI("\n")
return nil
}

// Perform logout.
if err := authManager.Logout(ctx, identityName); err != nil {
// Check if it's a partial logout.
if errors.Is(err, errUtils.ErrPartialLogout) {
u.PrintfMessageToTUI("✓ **Logged out with warnings**\n\n")
u.PrintfMessageToTUI("Some credentials could not be removed:\n")
u.PrintfMessageToTUI(" %v\n\n", err)
} else {
u.PrintfMessageToTUI("✗ **Logout failed**\n\n")
u.PrintfMessageToTUI("Error: %v\n\n", err)
return err
}
} else {
u.PrintfMessageToTUI("✓ **Successfully logged out**\n\n")
}

// Display browser session warning.
displayBrowserWarning()

return nil
}

// performProviderLogout removes credentials for a specific provider.
func performProviderLogout(ctx context.Context, authManager auth.AuthManager, providerName string, dryRun bool) error {
// Validate provider exists.
providers := authManager.GetProviders()
if _, exists := providers[providerName]; !exists {
u.PrintfMessageToTUI("**Error:** provider %q not found in configuration\n\n", providerName)
u.PrintfMessageToTUI("**Available providers:**\n")
for name := range providers {
u.PrintfMessageToTUI(" • %s\n", name)
}
u.PrintfMessageToTUI("\n")
return fmt.Errorf("%w: provider %q", errUtils.ErrProviderNotInConfig, providerName)
}

u.PrintfMessageToTUI("\n**Logging out from provider:** %s\n\n", providerName)

if dryRun {
u.PrintfMessageToTUI("**Dry run mode:** No credentials will be removed\n\n")
u.PrintfMessageToTUI("**Would remove:**\n")
u.PrintfMessageToTUI(" • All identities using provider %s\n", providerName)
u.PrintfMessageToTUI(" • Provider keyring entry\n")
// Get actual configured path for this provider.
basePath := authManager.GetFilesDisplayPath(providerName)
u.PrintfMessageToTUI(" • Files: %s/%s/\n", basePath, providerName)
u.PrintfMessageToTUI("\n")
return nil
}

// Perform logout.
if err := authManager.LogoutProvider(ctx, providerName); err != nil {
u.PrintfMessageToTUI("✗ **Provider logout failed**\n\n")
u.PrintfMessageToTUI("Error: %v\n\n", err)
return err
}

u.PrintfMessageToTUI("✓ **Successfully logged out from provider**\n\n")

// Display browser session warning.
displayBrowserWarning()

return nil
}

// performInteractiveLogout prompts user to choose what to logout.
func performInteractiveLogout(ctx context.Context, authManager auth.AuthManager, dryRun bool) error {
identities := authManager.GetIdentities()
providers := authManager.GetProviders()

if len(identities) == 0 {
u.PrintfMessageToTUI("**No identities configured** in atmos.yaml\n")
return nil
}

// Build options list.
type logoutOption struct {
label string
typ string // "identity", "provider", "all"
target string
}

var options []logoutOption

// Add identity options.
for name := range identities {
options = append(options, logoutOption{
label: fmt.Sprintf("Identity: %s", name),
typ: "identity",
target: name,
})
}

// Add provider options.
for name := range providers {
options = append(options, logoutOption{
label: fmt.Sprintf("Provider: %s (removes all identities)", name),
typ: "provider",
target: name,
})
}

// Add "all" option.
options = append(options, logoutOption{
label: "All identities (complete logout)",
typ: "all",
target: "",
})

// Create select options for huh.
var huhOptions []huh.Option[logoutOption]
for _, opt := range options {
huhOptions = append(huhOptions, huh.NewOption(opt.label, opt))
}

var selected logoutOption
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[logoutOption]().
Title("Choose what to logout from:").
Options(huhOptions...).
Value(&selected),
),
).WithTheme(uiutils.NewAtmosHuhTheme())

if err := form.Run(); err != nil {
return err
}

// Perform the selected logout action.
switch selected.typ {
case "identity":
return performIdentityLogout(ctx, authManager, selected.target, dryRun)
case "provider":
return performProviderLogout(ctx, authManager, selected.target, dryRun)
case "all":
return performLogoutAll(ctx, authManager, dryRun)
default:
return errUtils.ErrInvalidLogoutOption
}
}

// performLogoutAll removes all credentials.
func performLogoutAll(ctx context.Context, authManager auth.AuthManager, dryRun bool) error {
u.PrintfMessageToTUI("\n**Logging out from all identities**\n\n")

if dryRun {
u.PrintfMessageToTUI("**Dry run mode:** No credentials will be removed\n\n")
u.PrintfMessageToTUI("**Would remove:**\n")
u.PrintfMessageToTUI(" • All identity keyring entries\n")
u.PrintfMessageToTUI(" • All provider keyring entries\n")

// Show file paths for each provider.
providers := authManager.GetProviders()
if len(providers) > 0 {
u.PrintfMessageToTUI(" • Files:\n")
for providerName := range providers {
basePath := authManager.GetFilesDisplayPath(providerName)
u.PrintfMessageToTUI(" - %s/%s/\n", basePath, providerName)
}
}
u.PrintfMessageToTUI("\n")
return nil
}

// Perform logout.
if err := authManager.LogoutAll(ctx); err != nil {
u.PrintfMessageToTUI("✗ **Logout all failed**\n\n")
u.PrintfMessageToTUI("Error: %v\n\n", err)
return err
}

u.PrintfMessageToTUI("✓ **Successfully logged out from all identities**\n\n")

// Display browser session warning.
displayBrowserWarning()

return nil
}

// displayBrowserWarning shows a warning about browser sessions.
func displayBrowserWarning() {
u.PrintfMessageToTUI("⚠️ **Note:** This only removes local credentials.\n")
u.PrintfMessageToTUI(" Your browser session may still be active. Visit your\n")
u.PrintfMessageToTUI(" identity provider to end your browser session.\n\n")
}

func init() {
authLogoutCmd.Flags().String("provider", "", "Logout from specific provider")
authLogoutCmd.Flags().Bool("dry-run", false, "Preview what would be removed without deleting")
authCmd.AddCommand(authLogoutCmd)
}
Loading