Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ contexts:
| `assume_role` | Assume a role from a base profile | `profile`, `role_arn` |
| `sso` | Use AWS IAM Identity Center / SSO, reusing a valid AWS CLI SSO cache and prompting for login only when needed | `profile`, `sso_start_url`, and for concrete contexts `sso_account_id`, `sso_role_name` |

TUI startup is passive for SSO contexts: it loads the context picker without launching `aws sso login`. SSO login is prompted when you explicitly select or set up an SSO context, or when an AWS-backed workflow needs credentials.

Optional context fields:

| Field | Meaning |
Expand Down
21 changes: 20 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ type updateAvailableMsg struct {
method update.InstallMethod
}

var appLoadCallerIdentityFn = func(m Model) tea.Cmd {
return m.loadCallerIdentity()
}

func (m Model) checkForUpdate() tea.Cmd {
return func() tea.Msg {
method := update.DetectInstallMethod()
Expand All @@ -280,7 +284,22 @@ func (m Model) checkForUpdate() tea.Cmd {
}

func (m Model) Init() tea.Cmd {
return tea.Batch(m.loadContexts(), m.checkForUpdate(), m.loadCallerIdentity())
return tea.Batch(m.loadContexts(), m.checkForUpdate(), m.loadStartupCallerIdentity())
}

func (m Model) loadStartupCallerIdentity() tea.Cmd {
return func() tea.Msg {
if m.cfg == nil {
return callerIdentityMsg{}
}
if m.cfg.AuthType == config.AuthTypeSSO {
check, err := contextCheckSSOSessionFn(m.cfg)
if err != nil || check.LoginRequired {
return callerIdentityMsg{}
}
}
return appLoadCallerIdentityFn(m)()
}
}

func (m Model) loadCallerIdentity() tea.Cmd {
Expand Down
143 changes: 143 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,149 @@ func TestLoadingSpinnerTickUpdatesOnlyOnLoadingScreen(t *testing.T) {
}
}

func TestStartupCallerIdentitySkipsInteractiveSSOLoginWhenCacheMissing(t *testing.T) {
origCheck := contextCheckSSOSessionFn
origLoad := appLoadCallerIdentityFn
defer func() {
contextCheckSSOSessionFn = origCheck
appLoadCallerIdentityFn = origLoad
}()

checkCalled := false
loadCalled := false
contextCheckSSOSessionFn = func(cfg *config.Config) (awsservice.SSOSessionCheck, error) {
checkCalled = true
return awsservice.SSOSessionCheck{
StartURL: cfg.SSOStartURL,
LoginRequired: true,
}, nil
}
appLoadCallerIdentityFn = func(Model) tea.Cmd {
loadCalled = true
return func() tea.Msg {
return callerIdentityMsg{identity: &awsservice.CallerIdentity{Account: "123456789012"}}
}
}

m := New(&config.Config{
AuthType: config.AuthTypeSSO,
SSOStartURL: "https://example.awsapps.com/start",
Region: "us-east-1",
}, "", "dev")

msg := m.loadStartupCallerIdentity()()
if _, ok := msg.(callerIdentityMsg); !ok {
t.Fatalf("expected callerIdentityMsg, got %T", msg)
}
if !checkCalled {
t.Fatal("expected startup to check SSO cache")
}
if loadCalled {
t.Fatal("expected startup to skip identity loading when SSO login is required")
}
}

func TestStartupCallerIdentitySkipsInteractiveSSOLoginWhenSessionCheckFails(t *testing.T) {
origCheck := contextCheckSSOSessionFn
origLoad := appLoadCallerIdentityFn
defer func() {
contextCheckSSOSessionFn = origCheck
appLoadCallerIdentityFn = origLoad
}()

checkCalled := false
loadCalled := false
contextCheckSSOSessionFn = func(cfg *config.Config) (awsservice.SSOSessionCheck, error) {
checkCalled = true
return awsservice.SSOSessionCheck{}, os.ErrNotExist
}
appLoadCallerIdentityFn = func(Model) tea.Cmd {
loadCalled = true
return func() tea.Msg {
return callerIdentityMsg{identity: &awsservice.CallerIdentity{Account: "123456789012"}}
}
}

m := New(&config.Config{
AuthType: config.AuthTypeSSO,
SSOStartURL: "https://example.awsapps.com/start",
Region: "us-east-1",
}, "", "dev")

msg := m.loadStartupCallerIdentity()()
if _, ok := msg.(callerIdentityMsg); !ok {
t.Fatalf("expected callerIdentityMsg, got %T", msg)
}
if !checkCalled {
t.Fatal("expected startup to check SSO cache")
}
if loadCalled {
t.Fatal("expected startup to skip identity loading when SSO session check fails")
}
}

func TestStartupCallerIdentityLoadsSSOIdentityWhenCacheValid(t *testing.T) {
origCheck := contextCheckSSOSessionFn
origLoad := appLoadCallerIdentityFn
defer func() {
contextCheckSSOSessionFn = origCheck
appLoadCallerIdentityFn = origLoad
}()

contextCheckSSOSessionFn = func(cfg *config.Config) (awsservice.SSOSessionCheck, error) {
return awsservice.SSOSessionCheck{StartURL: cfg.SSOStartURL}, nil
}
appLoadCallerIdentityFn = func(Model) tea.Cmd {
return func() tea.Msg {
return callerIdentityMsg{identity: &awsservice.CallerIdentity{Account: "123456789012"}}
}
}

m := New(&config.Config{
AuthType: config.AuthTypeSSO,
SSOStartURL: "https://example.awsapps.com/start",
Region: "us-east-1",
}, "", "dev")

msg := m.loadStartupCallerIdentity()()
identityMsg, ok := msg.(callerIdentityMsg)
if !ok {
t.Fatalf("expected callerIdentityMsg, got %T", msg)
}
if identityMsg.identity == nil || identityMsg.identity.Account != "123456789012" {
t.Fatalf("expected loaded identity, got %#v", identityMsg.identity)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestStartupCallerIdentityStillLoadsNonSSOIdentity(t *testing.T) {
origLoad := appLoadCallerIdentityFn
defer func() {
appLoadCallerIdentityFn = origLoad
}()

loadCalled := false
appLoadCallerIdentityFn = func(Model) tea.Cmd {
loadCalled = true
return func() tea.Msg {
return callerIdentityMsg{identity: &awsservice.CallerIdentity{Account: "123456789012"}}
}
}

m := New(&config.Config{
AuthType: config.AuthTypeCredential,
Profile: "default",
Region: "us-east-1",
}, "", "dev")

msg := m.loadStartupCallerIdentity()()
if _, ok := msg.(callerIdentityMsg); !ok {
t.Fatalf("expected callerIdentityMsg, got %T", msg)
}
if !loadCalled {
t.Fatal("expected non-SSO startup identity loading to continue")
}
}

func TestListIndexHelpersWrapAndClamp(t *testing.T) {
if got := previousListIndex(0, 3); got != 2 {
t.Fatalf("expected previous from first to wrap to 2, got %d", got)
Expand Down
Loading