Skip to content
Draft
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
18 changes: 16 additions & 2 deletions pam/internal/adapter/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ type authenticationModel struct {
mode authd.SessionMode

inProgress bool
inputLocked bool
currentModel authenticationComponent
currentSessionID string
currentBrokerID string
Expand Down Expand Up @@ -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 {
Expand All @@ -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",
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 = ""
Expand Down
74 changes: 74 additions & 0 deletions pam/internal/adapter/authentication_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
9 changes: 8 additions & 1 deletion pam/internal/adapter/formmodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ type formModel struct {
focusableModels []authenticationComponent
focusIndex int

wait bool
wait bool
submitting bool
}

// newFormModel initializes and return a new formModel.
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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(),
Expand Down
Loading