diff --git a/examplebroker/broker.go b/examplebroker/broker.go index 4f4787113..353315530 100644 --- a/examplebroker/broker.go +++ b/examplebroker/broker.go @@ -162,6 +162,11 @@ func (b *Broker) NewSession(ctx context.Context, username, lang, mode string) (s exampleUsers[username] = userInfoBroker{Password: "goodpass"} } + if _, ok := exampleUsers[username]; !ok && strings.HasPrefix(username, "user-mfa-integration") { + exampleUsers[username] = userInfoBroker{Password: "goodpass"} + info.neededAuthSteps = 3 + } + pubASN1, err := x509.MarshalPKIXPublicKey(&b.privateKey.PublicKey) if err != nil { return "", "", err diff --git a/pam/integration-tests/gdm-module-handler_test.go b/pam/integration-tests/gdm-module-handler_test.go index 2a37d1e2c..26899a75d 100644 --- a/pam/integration-tests/gdm-module-handler_test.go +++ b/pam/integration-tests/gdm-module-handler_test.go @@ -23,7 +23,9 @@ type gdmTestModuleHandler struct { protoVersion uint32 - supportedLayouts []*authd.UILayout + supportedLayouts []*authd.UILayout + currentUILayout *authd.UILayout + selectedUILayouts []*authd.UILayout currentStage proto.Stage pollResponses []*gdm.EventData @@ -78,9 +80,19 @@ func (gh *gdmTestModuleHandler) exampleHandleGdmData(gdmData *gdm.Data) (*gdm.Da func (gh *gdmTestModuleHandler) exampleHandleEvent(event *gdm.EventData) error { events, ok := gh.eventPollResponses[event.Type] if ok && len(events) > 0 { - pollResp := events[0] - gh.eventPollResponses[event.Type] = slices.Delete(events, 0, 1) - gh.pollResponses = append(gh.pollResponses, pollResp) + numEvents := 1 + if events[0].Type == gdm_test.EventsGroupBegin().Type { + numEvents = slices.IndexFunc(events, func(ev *gdm.EventData) bool { + return ev.Type == gdm_test.EventsGroupEnd().Type + }) + require.Greater(gh.t, numEvents, 1, "No valid events group found") + events = slices.Delete(events, numEvents, numEvents+1) + events = slices.Delete(events, 0, 1) + numEvents-- + } + pollEvents := slices.Clone(events[0:numEvents]) + gh.eventPollResponses[event.Type] = slices.Delete(events, 0, numEvents) + gh.pollResponses = append(gh.pollResponses, pollEvents...) } switch ev := event.Data.(type) { @@ -126,6 +138,11 @@ func (gh *gdmTestModuleHandler) exampleHandleEvent(event *gdm.EventData) error { if layout.Label != nil { gh.t.Logf("%s:", *layout.Label) } + if layout.Content != nil { + gh.t.Logf("%s:", *layout.Content) + } + + gh.currentUILayout = layout case *gdm.EventData_StartAuthentication: idx := slices.IndexFunc(gh.authModes, func(mode *authd.GAMResponse_AuthenticationMode) bool { @@ -142,6 +159,15 @@ func (gh *gdmTestModuleHandler) exampleHandleEvent(event *gdm.EventData) error { "Selected authentication mode ID does not match expected one") gh.selectedAuthModeIDs = slices.Delete(gh.selectedAuthModeIDs, 0, 1) + if len(gh.selectedUILayouts) < 1 { + // TODO: Make this an error but we don't support checking the layout in all tests yet. + return nil + } + + gdm_test.RequireEqualData(gh.t, gh.selectedUILayouts[0], gh.currentUILayout, + "Selected UI layout does not match expected one") + gh.selectedUILayouts = slices.Delete(gh.selectedUILayouts, 0, 1) + case *gdm.EventData_AuthEvent: gh.t.Logf("Authentication event: %s", ev.AuthEvent.Response) if msg := ev.AuthEvent.Response.Msg; msg != "" { @@ -173,6 +199,14 @@ func (gh *gdmTestModuleHandler) exampleHandleAuthDRequest(gdmData *gdm.Data) (*g gh.currentStage = req.ChangeStage.Stage log.Debugf(context.TODO(), "Switching to stage %d", gh.currentStage) + switch req.ChangeStage.Stage { + case proto.Stage_brokerSelection: + gh.authModes = nil + gh.brokerID = "" + case proto.Stage_authModeSelection: + gh.currentUILayout = nil + } + return &gdm.Data{ Type: gdm.DataType_response, Response: &gdm.ResponseData{ diff --git a/pam/integration-tests/gdm_test.go b/pam/integration-tests/gdm_test.go index 70d6e7db1..484d159a8 100644 --- a/pam/integration-tests/gdm_test.go +++ b/pam/integration-tests/gdm_test.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "testing" "time" @@ -35,8 +36,29 @@ const ( passwordAuthID = "password" fido1AuthID = "fidodevice1" phoneAck1ID = "phoneack1" + qrcodeID = "qrcodewithtypo" ) +var testPasswordUILayout = authd.UILayout{ + Type: "form", + Label: ptrValue("Gimme your password"), + Entry: ptrValue("chars_password"), + Button: ptrValue(""), + Code: ptrValue(""), + Content: ptrValue(""), + Wait: ptrValue(""), +} + +var testQrcodeUILayout = authd.UILayout{ + Type: "qrcode", + Label: ptrValue("Enter the following code after flashing the address: 1337"), + Content: ptrValue("https://ubuntu.com"), + Wait: ptrValue("true"), + Button: ptrValue("Regenerate code"), + Code: ptrValue(""), + Entry: ptrValue(""), +} + func TestGdmModule(t *testing.T) { t.Parallel() t.Cleanup(pam_test.MaybeDoLeakCheck) @@ -54,19 +76,19 @@ func TestGdmModule(t *testing.T) { testCases := map[string]struct { supportedLayouts []*authd.UILayout - pamUser string + pamUser *string protoVersion uint32 brokerName string - authModeIDs []string eventPollResponses map[gdm.EventType][]*gdm.EventData wantError error + wantAuthModeIDs []string + wantUILayouts []*authd.UILayout wantPamInfoMessages []string wantPamErrorMessages []string wantAcctMgmtErr error }{ - "Authenticates user1": { - pamUser: "user1", + "Authenticates user": { eventPollResponses: map[gdm.EventType][]*gdm.EventData{ gdm.EventType_startAuthentication: { gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ @@ -75,9 +97,8 @@ func TestGdmModule(t *testing.T) { }, }, }, - "Authenticates user2 with multiple retries": { - pamUser: "user2", - authModeIDs: []string{passwordAuthID, passwordAuthID, passwordAuthID}, + "Authenticates user with multiple retries": { + wantAuthModeIDs: []string{passwordAuthID, passwordAuthID, passwordAuthID}, eventPollResponses: map[gdm.EventType][]*gdm.EventData{ gdm.EventType_startAuthentication: { gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ @@ -93,8 +114,8 @@ func TestGdmModule(t *testing.T) { }, }, "Authenticates user-mfa": { - pamUser: "user-mfa", - authModeIDs: []string{passwordAuthID, fido1AuthID, phoneAck1ID}, + pamUser: ptrValue("user-mfa"), + wantAuthModeIDs: []string{passwordAuthID, fido1AuthID, phoneAck1ID}, eventPollResponses: map[gdm.EventType][]*gdm.EventData{ gdm.EventType_startAuthentication: { gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ @@ -110,8 +131,8 @@ func TestGdmModule(t *testing.T) { }, }, "Authenticates user-mfa after retry": { - pamUser: "user-mfa", - authModeIDs: []string{passwordAuthID, passwordAuthID, fido1AuthID, phoneAck1ID}, + pamUser: ptrValue("user-mfa-integration-retry"), + wantAuthModeIDs: []string{passwordAuthID, passwordAuthID, fido1AuthID, phoneAck1ID}, eventPollResponses: map[gdm.EventType][]*gdm.EventData{ gdm.EventType_startAuthentication: { gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ @@ -129,25 +150,59 @@ func TestGdmModule(t *testing.T) { }, }, }, - "Authenticates user2 after switching to phone ack": { - pamUser: "user2", - authModeIDs: []string{passwordAuthID, phoneAck1ID}, + "Authenticates user switching to phone ack": { + wantAuthModeIDs: []string{passwordAuthID, phoneAck1ID}, eventPollResponses: map[gdm.EventType][]*gdm.EventData{ gdm.EventType_startAuthentication: { + gdm_test.EventsGroupBegin(), gdm_test.ChangeStageEvent(proto.Stage_authModeSelection), + gdm_test.AuthModeSelectedEvent(phoneAck1ID), + gdm_test.EventsGroupEnd(), + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Wait{ Wait: "true", }), }, - gdm.EventType_authEvent: { - gdm_test.AuthModeSelectedEvent(phoneAck1ID), + }, + }, + "Authenticates user with qrcode": { + wantAuthModeIDs: []string{qrcodeID}, + supportedLayouts: []*authd.UILayout{pam_test.QrCodeUILayout()}, + eventPollResponses: map[gdm.EventType][]*gdm.EventData{ + gdm.EventType_startAuthentication: { + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Wait{ + Wait: "true", + }), + }, + }, + wantUILayouts: []*authd.UILayout{&testQrcodeUILayout}, + }, + "Authenticates user after switching to qrcode": { + wantAuthModeIDs: []string{passwordAuthID, qrcodeID}, + supportedLayouts: []*authd.UILayout{ + pam_test.FormUILayout(), + pam_test.QrCodeUILayout(), + }, + eventPollResponses: map[gdm.EventType][]*gdm.EventData{ + gdm.EventType_startAuthentication: { + gdm_test.EventsGroupBegin(), + gdm_test.ChangeStageEvent(proto.Stage_authModeSelection), + gdm_test.AuthModeSelectedEvent(qrcodeID), + gdm_test.EventsGroupEnd(), + + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Wait{ + Wait: "true", + }), }, }, + wantUILayouts: []*authd.UILayout{ + &testPasswordUILayout, + &testQrcodeUILayout, + }, }, // Error cases "Error on unknown protocol": { - pamUser: "user-foo", protoVersion: 9999, wantPamErrorMessages: []string{ "GDM protocol initialization failed, type hello, version 9999", @@ -156,7 +211,7 @@ func TestGdmModule(t *testing.T) { wantAcctMgmtErr: pam_test.ErrIgnore, }, "Error on missing user": { - pamUser: "", + pamUser: ptrValue(""), wantPamErrorMessages: []string{ "can't select broker: rpc error: code = InvalidArgument desc = can't start authentication transaction: rpc error: code = InvalidArgument desc = no user name provided", }, @@ -164,7 +219,6 @@ func TestGdmModule(t *testing.T) { wantAcctMgmtErr: pam_test.ErrIgnore, }, "Error on no supported layouts": { - pamUser: "user-bar", supportedLayouts: []*authd.UILayout{}, wantPamErrorMessages: []string{ "UI does not support any layouts", @@ -173,7 +227,6 @@ func TestGdmModule(t *testing.T) { wantAcctMgmtErr: pam_test.ErrIgnore, }, "Error on unknown broker": { - pamUser: "user-foo", brokerName: "Not a valid broker!", eventPollResponses: map[gdm.EventType][]*gdm.EventData{ gdm.EventType_brokersReceived: { @@ -187,7 +240,6 @@ func TestGdmModule(t *testing.T) { wantAcctMgmtErr: pam_test.ErrIgnore, }, "Error (ignored) on local broker causes fallback error": { - pamUser: "user-foo", brokerName: brokers.LocalBrokerName, wantPamInfoMessages: []string{ "auth=incomplete", @@ -195,9 +247,8 @@ func TestGdmModule(t *testing.T) { wantError: pam_test.ErrIgnore, wantAcctMgmtErr: pam.ErrAbort, }, - "Error on authenticating user2 with too many retries": { - pamUser: "user2", - authModeIDs: []string{ + "Error on authenticating user with too many retries": { + wantAuthModeIDs: []string{ passwordAuthID, passwordAuthID, passwordAuthID, @@ -234,7 +285,7 @@ func TestGdmModule(t *testing.T) { wantAcctMgmtErr: pam_test.ErrIgnore, }, "Error on authenticating unknown user": { - pamUser: "user-unknown", + pamUser: ptrValue("user-unknown"), eventPollResponses: map[gdm.EventType][]*gdm.EventData{ gdm.EventType_startAuthentication: { gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ @@ -249,8 +300,8 @@ func TestGdmModule(t *testing.T) { wantAcctMgmtErr: pam_test.ErrIgnore, }, "Error on invalid fido ack": { - pamUser: "user-mfa", - authModeIDs: []string{passwordAuthID, fido1AuthID}, + pamUser: ptrValue("user-mfa-integration-error-fido-ack"), + wantAuthModeIDs: []string{passwordAuthID, fido1AuthID}, eventPollResponses: map[gdm.EventType][]*gdm.EventData{ gdm.EventType_startAuthentication: { gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ @@ -290,10 +341,21 @@ func TestGdmModule(t *testing.T) { serviceFile := createServiceFile(t, "gdm-authd", libPath, moduleArgs) - gh := newGdmTestModuleHandler(t, serviceFile, tc.pamUser) - t.Cleanup(func() { require.NoError(t, gh.tx.End(), "PAM: can't end transaction") }) + pamUser := "user-integration-" + strings.ReplaceAll(filepath.Base(t.Name()), "_", "-") + if tc.pamUser != nil { + pamUser = *tc.pamUser + } + + timedOut := false + gh := newGdmTestModuleHandler(t, serviceFile, pamUser) + t.Cleanup(func() { + if !timedOut { + require.NoError(t, gh.tx.End(), "PAM: can't end transaction") + } + }) gh.eventPollResponses = tc.eventPollResponses + gh.supportedLayouts = tc.supportedLayouts if tc.supportedLayouts == nil { gh.supportedLayouts = []*authd.UILayout{pam_test.FormUILayout()} } @@ -308,11 +370,18 @@ func TestGdmModule(t *testing.T) { gh.selectedBrokerName = exampleBrokerName } - gh.selectedAuthModeIDs = tc.authModeIDs + gh.selectedAuthModeIDs = tc.wantAuthModeIDs if gh.selectedAuthModeIDs == nil { gh.selectedAuthModeIDs = []string{passwordAuthID} } + gh.selectedUILayouts = tc.wantUILayouts + if gh.selectedAuthModeIDs == nil && + len(gh.selectedAuthModeIDs) == 1 && + gh.selectedAuthModeIDs[0] == passwordAuthID { + gh.selectedUILayouts = []*authd.UILayout{&testPasswordUILayout} + } + var pamFlags pam.Flags if !testutils.IsVerbose() { pamFlags = pam.Silent @@ -325,7 +394,8 @@ func TestGdmModule(t *testing.T) { var err error select { - case <-time.After(10 * time.Second): + case <-time.After(30 * time.Second): + timedOut = true t.Fatal("Authentication timed out!") case err = <-authResult: } @@ -336,19 +406,19 @@ func TestGdmModule(t *testing.T) { require.Equal(t, tc.wantPamInfoMessages, gh.pamInfoMessages, "PAM Info messages do not match") - requirePreviousBrokerForUser(t, socketPath, "", tc.pamUser) + requirePreviousBrokerForUser(t, socketPath, "", pamUser) require.ErrorIs(t, gh.tx.AcctMgmt(pamFlags), tc.wantAcctMgmtErr, "Account Management PAM Error messages do not match") if tc.wantError != nil { - requirePreviousBrokerForUser(t, socketPath, "", tc.pamUser) + requirePreviousBrokerForUser(t, socketPath, "", pamUser) return } user, err := gh.tx.GetItem(pam.User) require.NoError(t, err, "Can't get the pam user") - require.Equal(t, tc.pamUser, user, "PAM user name does not match expected") + require.Equal(t, pamUser, user, "PAM user name does not match expected") requirePreviousBrokerForUser(t, socketPath, gh.selectedBrokerName, user) }) @@ -378,7 +448,7 @@ func TestGdmModuleAuthenticateWithoutGdmExtension(t *testing.T) { moduleArgs = append(moduleArgs, "debug=true", "logfile="+gdmLog) serviceFile := createServiceFile(t, "gdm-authd", libPath, moduleArgs) - pamUser := "user1" + pamUser := "user-integration-auth-no-gdm-extension" gh := newGdmTestModuleHandler(t, serviceFile, pamUser) t.Cleanup(func() { require.NoError(t, gh.tx.End(), "PAM: can't end transaction") }) @@ -420,7 +490,7 @@ func TestGdmModuleAcctMgmtWithoutGdmExtension(t *testing.T) { moduleArgs = append(moduleArgs, "debug=true", "logfile="+gdmLog) serviceFile := createServiceFile(t, "gdm-authd", libPath, moduleArgs) - pamUser := "user1" + pamUser := "user-integration-acctmgmt-no-gdm-extension" gh := newGdmTestModuleHandler(t, serviceFile, pamUser) t.Cleanup(func() { require.NoError(t, gh.tx.End(), "PAM: can't end transaction") }) diff --git a/pam/internal/adapter/gdmmodel.go b/pam/internal/adapter/gdmmodel.go index ada7fee90..29612903e 100644 --- a/pam/internal/adapter/gdmmodel.go +++ b/pam/internal/adapter/gdmmodel.go @@ -141,9 +141,7 @@ func (m *gdmModel) pollGdm() tea.Cmd { commands = append(commands, sendEvent(reselectAuthMode{})) case *gdm.EventData_IsAuthenticatedCancelled: - if m.waitingAuth { - commands = append(commands, sendEvent(isAuthenticatedCancelled{})) - } + commands = append(commands, sendEvent(isAuthenticatedCancelled{})) case *gdm.EventData_StageChanged: if res.StageChanged == nil { @@ -232,6 +230,9 @@ func (m gdmModel) Update(msg tea.Msg) (gdmModel, tea.Cmd) { StartAuthentication: &gdm.Events_StartAuthentication{}, })) + case reselectAuthMode: + m.waitingAuth = false + case isAuthenticatedResultReceived: access := msg.access authMsg, err := dataToMsg(msg.msg) diff --git a/pam/internal/adapter/gdmmodel_test.go b/pam/internal/adapter/gdmmodel_test.go index f27a534d7..54e263066 100644 --- a/pam/internal/adapter/gdmmodel_test.go +++ b/pam/internal/adapter/gdmmodel_test.go @@ -83,6 +83,7 @@ func TestGdmModel(t *testing.T) { pamUser string protoVersion uint32 convError map[string]error + timeout time.Duration wantExitStatus PamReturnStatus wantGdmRequests []gdm.RequestType @@ -914,6 +915,162 @@ func TestGdmModel(t *testing.T) { gdm.EventType_uiLayoutReceived, gdm.EventType_startAuthentication, gdm.EventType_authEvent, + gdm.EventType_authEvent, + }, + wantStage: pam_proto.Stage_challenge, + wantGdmAuthRes: []*authd.IAResponse{ + {Access: brokers.AuthCancelled}, + {Access: brokers.AuthGranted}, + }, + wantExitStatus: PamSuccess{BrokerID: firstBrokerInfo.Id}, + }, + "Authenticated with qrcode after auth selection stage from client after client-side broker and auth mode selection": { + supportedLayouts: []*authd.UILayout{ + pam_test.FormUILayout(), + pam_test.QrCodeUILayout(), + }, + clientOptions: append(slices.Clone(singleBrokerClientOptions), + pam_test.WithUILayout("qrcode", "Hello QR!", pam_test.QrCodeUILayout()), + pam_test.WithIsAuthenticatedWantWait(time.Millisecond*500), + ), + gdmEvents: []*gdm.EventData{ + gdm_test.SelectUserEvent("gdm-selected-user-broker-and-auth-mode"), + }, + messages: []tea.Msg{ + gdmTestWaitForStage{ + stage: pam_proto.Stage_brokerSelection, + events: []*gdm.EventData{ + gdm_test.SelectBrokerEvent(firstBrokerInfo.Id), + }, + }, + gdmTestWaitForStage{ + stage: pam_proto.Stage_challenge, + events: []*gdm.EventData{ + gdm_test.ChangeStageEvent(pam_proto.Stage_authModeSelection), + }, + commands: []tea.Cmd{ + sendEvent(gdmTestWaitForStage{ + stage: pam_proto.Stage_authModeSelection, + events: []*gdm.EventData{ + gdm_test.AuthModeSelectedEvent("qrcode"), + }, + commands: []tea.Cmd{ + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Wait{ + Wait: "true", + }}), + }, + }), + }, + }, + }, + wantUsername: "gdm-selected-user-broker-and-auth-mode", + wantSelectedBroker: firstBrokerInfo.Id, + wantGdmRequests: []gdm.RequestType{ + gdm.RequestType_uiLayoutCapabilities, + gdm.RequestType_changeStage, // -> broker Selection + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + }, + wantMessages: []tea.Msg{ + startAuthentication{}, + startAuthentication{}, + }, + wantGdmEvents: []gdm.EventType{ + gdm.EventType_userSelected, + gdm.EventType_brokersReceived, + gdm.EventType_brokerSelected, + gdm.EventType_authModeSelected, + gdm.EventType_uiLayoutReceived, + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, + gdm.EventType_authModeSelected, + gdm.EventType_uiLayoutReceived, + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, + }, + wantStage: pam_proto.Stage_challenge, + wantGdmAuthRes: []*authd.IAResponse{ + {Access: brokers.AuthCancelled}, + {Access: brokers.AuthGranted}, + }, + wantExitStatus: PamSuccess{BrokerID: firstBrokerInfo.Id}, + }, + "Authenticated with qrcode regenerated after auth selection stage from client after client-side broker and auth mode selection": { + timeout: 10 * time.Second, + supportedLayouts: []*authd.UILayout{ + pam_test.FormUILayout(), + pam_test.QrCodeUILayout(), + }, + clientOptions: append(slices.Clone(singleBrokerClientOptions), + pam_test.WithUILayout("qrcode", "Hello QR!", pam_test.QrCodeUILayout()), + pam_test.WithIsAuthenticatedWantWait(time.Millisecond*500), + ), + gdmEvents: []*gdm.EventData{ + gdm_test.SelectUserEvent("gdm-selected-user-broker-and-auth-mode"), + }, + messages: []tea.Msg{ + gdmTestWaitForStage{ + stage: pam_proto.Stage_brokerSelection, + events: []*gdm.EventData{ + gdm_test.SelectBrokerEvent(firstBrokerInfo.Id), + }, + }, + gdmTestWaitForStage{ + stage: pam_proto.Stage_challenge, + events: []*gdm.EventData{ + gdm_test.ChangeStageEvent(pam_proto.Stage_authModeSelection), + }, + commands: []tea.Cmd{ + sendEvent(gdmTestWaitForStage{ + stage: pam_proto.Stage_authModeSelection, + events: []*gdm.EventData{ + gdm_test.AuthModeSelectedEvent("qrcode"), + }, + commands: []tea.Cmd{ + sendEvent(gdmTestSendAuthDataWhenReady{}), + sendEvent(gdmTestWaitForStage{ + stage: pam_proto.Stage_challenge, + events: []*gdm.EventData{ + gdm_test.ReselectAuthMode(), + }, + }), + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Wait{ + Wait: "true", + }}), + }, + }), + }, + }, + }, + wantUsername: "gdm-selected-user-broker-and-auth-mode", + wantSelectedBroker: firstBrokerInfo.Id, + wantGdmRequests: []gdm.RequestType{ + gdm.RequestType_uiLayoutCapabilities, + gdm.RequestType_changeStage, // -> broker Selection + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + }, + wantMessages: []tea.Msg{ + startAuthentication{}, + startAuthentication{}, + startAuthentication{}, + }, + wantGdmEvents: []gdm.EventType{ + gdm.EventType_userSelected, + gdm.EventType_brokersReceived, + gdm.EventType_brokerSelected, + gdm.EventType_authModeSelected, + gdm.EventType_uiLayoutReceived, + gdm.EventType_startAuthentication, + gdm.EventType_authModeSelected, + gdm.EventType_authEvent, + gdm.EventType_uiLayoutReceived, + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, }, wantStage: pam_proto.Stage_challenge, wantGdmAuthRes: []*authd.IAResponse{ @@ -1851,7 +2008,6 @@ func TestGdmModel(t *testing.T) { p := tea.NewProgram(&appState, teaOpts...) appState.program = p - // testHadTimeout := false controlDone := make(chan struct{}) go func() { wg := sync.WaitGroup{} @@ -1897,13 +2053,16 @@ func TestGdmModel(t *testing.T) { } t.Log("Waiting for expected events") + if tc.timeout == 0 { + tc.timeout = 5 * time.Second + } waitChan := make(chan struct{}) go func() { wg.Wait() close(waitChan) }() select { - case <-time.After(5 * time.Second): + case <-time.After(tc.timeout): case <-waitChan: } t.Log("Waiting for events done...") diff --git a/pam/internal/adapter/gdmmodel_uimodel_test.go b/pam/internal/adapter/gdmmodel_uimodel_test.go index 38360c322..8860fbda1 100644 --- a/pam/internal/adapter/gdmmodel_uimodel_test.go +++ b/pam/internal/adapter/gdmmodel_uimodel_test.go @@ -141,7 +141,9 @@ func (m *gdmTestUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { go func() { m.gdmHandler.waitForAuthenticationStarted() - m.gdmHandler.appendPollResultEvents(gdm_test.IsAuthenticatedEvent(msg.item)) + if msg.item != nil { + m.gdmHandler.appendPollResultEvents(gdm_test.IsAuthenticatedEvent(msg.item)) + } m.program.Send(tea.Sequence(tea.Tick(gdmPollFrequency, func(t time.Time) tea.Msg { return sendEvent(doneMsg) }), sendEvent(doneMsg))()) diff --git a/pam/internal/gdm/extension.go b/pam/internal/gdm/extension.go index 0963a7ff3..f593c5814 100644 --- a/pam/internal/gdm/extension.go +++ b/pam/internal/gdm/extension.go @@ -8,14 +8,12 @@ package gdm import "C" import ( - "context" "encoding/json" "errors" "fmt" "unsafe" "github.com/msteinert/pam/v2" - "github.com/ubuntu/authd/internal/log" ) const ( @@ -147,7 +145,6 @@ func NewBinaryJSONProtoRequest(data []byte) (*pam.BinaryConvRequest, error) { if err != nil { return nil, err } - log.Debugf(context.TODO(), "Sending to gdm %s", string(data)) return pam.NewBinaryConvRequest(request.encode(), func(ptr pam.BinaryPointer) { (*jsonProtoMessage)(ptr).release() }), nil } diff --git a/pam/internal/gdm_test/gdm_utils.go b/pam/internal/gdm_test/gdm_utils.go index 65794b506..7bfa17273 100644 --- a/pam/internal/gdm_test/gdm_utils.go +++ b/pam/internal/gdm_test/gdm_utils.go @@ -11,7 +11,7 @@ import ( ) // RequireEqualData ensures that data is equal by checking the marshalled values. -func RequireEqualData(t *testing.T, want any, actual any) { +func RequireEqualData(t *testing.T, want any, actual any, args ...any) { t.Helper() wantJSON, err := json.MarshalIndent(want, "", " ") @@ -19,7 +19,7 @@ func RequireEqualData(t *testing.T, want any, actual any) { actualJSON, err := json.MarshalIndent(actual, "", " ") require.NoError(t, err) - require.Equal(t, string(wantJSON), string(actualJSON)) + require.Equal(t, string(wantJSON), string(actualJSON), args...) } // DataToJSON is a test helper function to convert GDM data to JSON. @@ -31,6 +31,24 @@ func DataToJSON(t *testing.T, data *gdm.Data) string { return string(json) } +// EventsGroupBegin returns a fake [gdm.EventData] that allows to begin a group multiple events +// so that it's possible to use this as an header to tell the test module handler that we should +// respond to an event with multiple events starting from the next one. +func EventsGroupBegin() *gdm.EventData { + return &gdm.EventData{ + Type: gdm.EventType(-1000), + } +} + +// EventsGroupEnd returns a fake [gdm.EventData] that allows to end a group multiple events +// so that it's possible to use this as a footer to tell the test module handler that we should +// respond to an event with multiple events finishing with the previous one. +func EventsGroupEnd() *gdm.EventData { + return &gdm.EventData{ + Type: gdm.EventType(-1001), + } +} + // SelectUserEvent generates a SelectUser event. func SelectUserEvent(username string) *gdm.EventData { return &gdm.EventData{ @@ -73,6 +91,16 @@ func AuthModeSelectedEvent(authModeID string) *gdm.EventData { } } +// ReselectAuthMode generates a ReselectAuthMode event. +func ReselectAuthMode() *gdm.EventData { + return &gdm.EventData{ + Type: gdm.EventType_reselectAuthMode, + Data: &gdm.EventData_ReselectAuthMode{ + ReselectAuthMode: &gdm.Events_ReselectAuthMode{}, + }, + } +} + // IsAuthenticatedEvent generates a IsAuthenticated event. func IsAuthenticatedEvent(item authd.IARequestAuthenticationDataItem) *gdm.EventData { return &gdm.EventData{ diff --git a/pam/internal/pam_test/pam-client-dummy.go b/pam/internal/pam_test/pam-client-dummy.go index e27d83c8d..07b9a320e 100644 --- a/pam/internal/pam_test/pam-client-dummy.go +++ b/pam/internal/pam_test/pam-client-dummy.go @@ -428,7 +428,14 @@ func (dc *DummyClient) IsAuthenticated(ctx context.Context, in *authd.IARequest, if dc.isAuthenticatedWantWait == 0 { return nil, errors.New("no wanted wait provided") } - time.Sleep(dc.isAuthenticatedWantWait) + select { + case <-time.After(dc.isAuthenticatedWantWait): + case <-ctx.Done(): + return &authd.IAResponse{ + Access: brokers.AuthCancelled, + Msg: fmt.Sprintf(`{"message": "Cancelled: %s"}`, dc.isAuthenticatedMessage), + }, nil + } return &authd.IAResponse{ Access: brokers.AuthGranted, Msg: msg,