diff --git a/pam/internal/adapter/authentication.go b/pam/internal/adapter/authentication.go index 2e2580525d..39e0e03b09 100644 --- a/pam/internal/adapter/authentication.go +++ b/pam/internal/adapter/authentication.go @@ -126,6 +126,7 @@ type authenticationModel struct { mode authd.SessionMode inProgress bool + inputLocked bool currentModel authenticationComponent currentSessionID string currentBrokerID string @@ -203,6 +204,8 @@ func (m *authenticationModel) cancelIsAuthenticated() tea.Cmd { // Update handles events and actions. func (m authenticationModel) Update(msg tea.Msg) (authModel authenticationModel, command tea.Cmd) { + var focusCmd tea.Cmd + switch msg := msg.(type) { case StageChanged: if msg.Stage != pam_proto.Stage_challenge { @@ -218,10 +221,14 @@ func (m authenticationModel) Update(msg tea.Msg) (authModel authenticationModel, case startAuthentication: safeMessageDebug(msg, "current model %v, focused %v", m.currentModel, m.Focused()) - if !m.Focused() { + if !m.Focused() && !m.inputLocked { return m, nil } m.inProgress = true + if m.inputLocked { + m.inputLocked = false + focusCmd = m.currentModel.Focus() + } case stopAuthentication: safeMessageDebug(msg, "current model %v, focused %v", @@ -285,6 +292,12 @@ func (m authenticationModel) Update(msg tea.Msg) (authModel authenticationModel, case isAuthenticatedRequested: safeMessageDebug(msg) + // Hide the input if we are in interactive terminal mode and the authentication request is for a secret (password). + if _, hasSecret := msg.item.(*authd.IARequest_AuthenticationData_Secret); hasSecret && m.clientType == InteractiveTerminal { + m.inputLocked = true + m.Blur() + } + authTracker := m.authTracker ctx, cancel := context.WithCancel(context.Background()) @@ -457,7 +470,7 @@ func (m authenticationModel) Update(msg tea.Msg) (authModel authenticationModel, model, cmd = m.currentModel.Update(msg) m.currentModel = convertTo[authenticationComponent](model) } - return m, cmd + return m, tea.Batch(focusCmd, cmd) } // Focus focuses this model. @@ -561,6 +574,7 @@ func (m authenticationModel) View() string { func (m *authenticationModel) Reset() tea.Cmd { log.Debugf(context.TODO(), "%T: Reset", m) m.inProgress = false + m.inputLocked = false m.currentModel = nil m.currentSessionID = "" m.currentBrokerID = "" diff --git a/pam/internal/adapter/authentication_test.go b/pam/internal/adapter/authentication_test.go new file mode 100644 index 0000000000..8e3dcfffa4 --- /dev/null +++ b/pam/internal/adapter/authentication_test.go @@ -0,0 +1,74 @@ +package adapter + +import ( + "testing" + + "github.com/canonical/authd/internal/brokers/layouts" + "github.com/canonical/authd/internal/brokers/layouts/entries" + "github.com/canonical/authd/internal/proto/authd" + tea "github.com/charmbracelet/bubbletea" + "github.com/stretchr/testify/require" +) + +func TestAuthenticationModelLocksTerminalInputWhileAuthenticating(t *testing.T) { + t.Parallel() + + entry := newTextInputModel(entries.CharsPassword) + entry.SetValue("password") + form := formModel{focusableModels: []authenticationComponent{&entry}} + + model := newAuthenticationModel(nil, InteractiveTerminal, authd.SessionMode_LOGIN) + model.currentModel = form + model.currentModel.Focus() + + updated, _ := model.Update(isAuthenticatedRequested{ + item: &authd.IARequest_AuthenticationData_Secret{Secret: "password"}, + }) + require.True(t, updated.inputLocked) + require.False(t, updated.Focused()) + + updated, _ = updated.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("x")}) + require.False(t, updated.Focused()) + require.Equal(t, "password", entry.Value()) + + updated, _ = updated.Update(startAuthentication{}) + require.False(t, updated.inputLocked) + require.True(t, updated.Focused()) +} + +func TestAuthenticationModelKeepsWaitLayoutVisibleWhileAuthenticating(t *testing.T) { + t.Parallel() + + entry := newTextInputModel(entries.CharsPassword) + form := formModel{focusableModels: []authenticationComponent{&entry}} + + model := newAuthenticationModel(nil, InteractiveTerminal, authd.SessionMode_LOGIN) + model.currentModel = form + model.currentModel.Focus() + + updated, _ := model.Update(isAuthenticatedRequested{ + item: &authd.IARequest_AuthenticationData_Wait{Wait: layouts.True}, + }) + require.False(t, updated.inputLocked) + require.True(t, updated.Focused()) +} + +func TestFormModelLocksInputOnSubmission(t *testing.T) { + t.Parallel() + + entry := newTextInputModel(entries.CharsPassword) + entry.SetValue("password") + form := formModel{focusableModels: []authenticationComponent{&entry}} + form.Focus() + + updated, _ := form.Update(tea.KeyMsg{Type: tea.KeyEnter}) + updatedForm, ok := updated.(formModel) + require.True(t, ok) + require.True(t, updatedForm.submitting) + + updated, _ = updatedForm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("x")}) + updatedForm, ok = updated.(formModel) + require.True(t, ok) + require.True(t, updatedForm.submitting) + require.Equal(t, "password", entry.Value()) +} diff --git a/pam/internal/adapter/formmodel.go b/pam/internal/adapter/formmodel.go index 5cd117f48e..728ac96e5f 100644 --- a/pam/internal/adapter/formmodel.go +++ b/pam/internal/adapter/formmodel.go @@ -21,7 +21,8 @@ type formModel struct { focusableModels []authenticationComponent focusIndex int - wait bool + wait bool + submitting bool } // newFormModel initializes and return a new formModel. @@ -61,6 +62,7 @@ func (m formModel) Init() tea.Cmd { func (m formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.(type) { case startAuthentication: + m.submitting = false // Reset the entry. for _, fm := range m.focusableModels { switch entry := fm.(type) { @@ -80,6 +82,10 @@ func (m formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { // Key presses case tea.KeyMsg: + if m.submitting { + return m, nil + } + switch msg.String() { case "enter", "ctrl+d": if m.focusIndex >= len(m.focusableModels) { @@ -95,6 +101,7 @@ func (m formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) } + m.submitting = true return m, sendEvent(isAuthenticatedRequested{ item: &authd.IARequest_AuthenticationData_Secret{ Secret: entry.Value(),