Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
104 changes: 104 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,110 @@ 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 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