diff --git a/.github/workflows/e2e-tests-provision-and-run.yaml b/.github/workflows/e2e-tests-provision-and-run.yaml index 00d3f0f081..6288c50711 100644 --- a/.github/workflows/e2e-tests-provision-and-run.yaml +++ b/.github/workflows/e2e-tests-provision-and-run.yaml @@ -20,6 +20,11 @@ on: type: string required: false default: '' + authd-ppa: + description: 'If set, use this PPA instead of authd-edge when installing the authd package and its dependencies' + type: string + required: false + default: '' secrets: E2E_VM_SSH_PRIV_KEY: required: true @@ -152,4 +157,5 @@ jobs: ubuntu-version: ${{ inputs.ubuntu-version }} broker: ${{ matrix.broker }} broker-snap-channel: ${{ inputs.broker-snap-channel }} + authd-ppa: ${{ inputs.authd-ppa }} secrets: inherit diff --git a/.github/workflows/e2e-tests-run.yaml b/.github/workflows/e2e-tests-run.yaml index fa8ae23197..e8ee15dfe3 100644 --- a/.github/workflows/e2e-tests-run.yaml +++ b/.github/workflows/e2e-tests-run.yaml @@ -14,6 +14,11 @@ on: required: false type: string default: '' + authd-ppa: + description: 'If set, use this PPA instead of authd-edge when installing the authd package and its dependencies' + required: false + type: string + default: '' secrets: E2E_VM_SSH_PRIV_KEY: required: true @@ -199,6 +204,7 @@ jobs: # broker credentials from the environment) ${{ env.E2E_TESTS_DIR }}/vm/provision-authd.sh \ --authd-deb "${{ steps.download-authd-deb.outputs.deb }}" \ + ${{ inputs.authd-ppa && format('--authd-ppa {0}', inputs.authd-ppa) || '' }} \ --broker-snap "${{ steps.download-broker-snap-from-store.outputs.snap || steps.download-broker-snap-from-oci.outputs.snap }}" - name: Checkout YARF repo diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 831d106aad..99d926feb5 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -53,6 +53,7 @@ on: - synchronize - reopened - labeled + - edited workflow_dispatch: inputs: broker-snap-channel: @@ -110,4 +111,14 @@ jobs: files-hash: ${{ needs.compute-hash.outputs.files-hash }} force-fresh-image: ${{ github.event_name == 'schedule' }} broker-snap-channel: ${{ inputs.broker-snap-channel || '' }} + # PRs opt in by adding "e2e-ppa: authd-dev" to their description. + authd-ppa: >- + ${{ + github.event_name == 'pull_request' && + contains( + github.event.pull_request.body, + 'e2e-ppa: authd-dev' + ) && + 'ubuntu-enterprise-desktop/authd-dev' || '' + }} secrets: inherit diff --git a/debian/authd.service.in b/debian/authd.service.in index f4a635bda5..453ff54c82 100644 --- a/debian/authd.service.in +++ b/debian/authd.service.in @@ -3,6 +3,8 @@ Description=authd daemon service After=authd.socket Requires=authd.socket PartOf=authd.socket +# AccountsService starts after this target to populate the GDM user list. +Before=nss-user-lookup.target [Service] Type=notify diff --git a/debian/authd.socket b/debian/authd.socket index 9461261261..91f911ebc1 100644 --- a/debian/authd.socket +++ b/debian/authd.socket @@ -6,6 +6,8 @@ Wants=dbus.service After=dbus.service Before=nss-user-lookup.target Wants=nss-user-lookup.target +# Start the daemon before AccountsService queries NSS for its cached users. +Wants=authd.service [Socket] ListenStream=/run/authd.sock diff --git a/debian/control b/debian/control index 4e00755d91..a67c466c46 100644 --- a/debian/control +++ b/debian/control @@ -48,7 +48,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, Recommends: ${misc:Recommends}, libpam-modules, -Breaks: gnome-shell (<< 46.3.1-1ubuntu1~24.04.1authd6~) +Breaks: ${authd:gnome-shell-breaks} Description: ${source:Synopsis} ${source:Extended-Description} . diff --git a/debian/rules b/debian/rules index e3171114ab..24f8271c31 100755 --- a/debian/rules +++ b/debian/rules @@ -4,6 +4,16 @@ include /usr/share/dpkg/pkg-info.mk include /usr/share/rustc/architecture.mk +ifeq ($(DEB_DISTRIBUTION),noble) +GNOME_SHELL_BREAKS := gnome-shell (<< 46.3.1-1ubuntu1+authd1~24.04.1) +else ifeq ($(DEB_DISTRIBUTION),resolute) +GNOME_SHELL_BREAKS := gnome-shell (<< 50.1-0ubuntu1.1+authd2~26.04.1) +else ifeq ($(DEB_DISTRIBUTION),stonking) +GNOME_SHELL_BREAKS := gnome-shell (<< 50.2-3ubuntu1+authd1~26.10.1) +else +$(error Unsupported distribution "$(DEB_DISTRIBUTION)" for gnome-shell dependency) +endif + # The package name for authd, used only locally to avoid repetitions AUTHD_GO_PACKAGE := $(shell grep-dctrl -s XS-Go-Import-Path -n - ./debian/control) @@ -60,6 +70,9 @@ export BUILT_PAM_LIBS_PATH := obj-$(DEB_HOST_GNU_TYPE)/src/$(AUTHD_GO_PACKAGE)/p # --without=single-binary can be removed with dh 15. dh $@ --buildsystem=golang --with=golang,apport --without=single-binary +override_dh_gencontrol: + dh_gencontrol -- -Vauthd:gnome-shell-breaks="$(GNOME_SHELL_BREAKS)" + override_dh_auto_clean: dh_auto_clean diff --git a/e2e-tests/vm/provision-authd.sh b/e2e-tests/vm/provision-authd.sh index 9fe2cb25bc..0b3874a49a 100755 --- a/e2e-tests/vm/provision-authd.sh +++ b/e2e-tests/vm/provision-authd.sh @@ -9,7 +9,7 @@ DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/authd-e2e-tests" usage(){ cat << EOF -Usage: $0 [--config-file ] [--release ] [--authd-deb ] [--broker-snap ] +Usage: $0 [--config-file ] [--release ] [--authd-deb ] [--authd-ppa ] [--broker-snap ] Options: --config-file Path to the configuration file (default: config.env) @@ -18,6 +18,8 @@ Options: The existing snapshots will be deleted and recreated with the new installation. --broker The broker to install ("authd-google", "authd-msentraid", ...) --authd-deb Path to the authd deb file to install (default: install from the edge PPA) + --authd-ppa PPA to use instead of authd-edge when installing authd + and its dependencies --broker-snap Path to the broker snap file to install (default: install from the edge channel) -h, --help Show this help message and exit @@ -47,6 +49,10 @@ while [[ $# -gt 0 ]]; do AUTHD_DEB="$2" shift 2 ;; + --authd-ppa) + AUTHD_PPA="$2" + shift 2 + ;; --broker-snap) BROKER_SNAP="$2" shift 2 @@ -292,9 +298,9 @@ fi # Revert to the pre-authd setup snapshot before installing the version to test restore_snapshot_and_sync_time "$PRE_AUTHD_SNAPSHOT" -# Add the edge PPA. We also need that when installing authd from a deb file, -# because it depends on gnome-shell from the edge PPA. -PPA="ubuntu-enterprise-desktop/authd-edge" +# Add the PPA needed to resolve dependencies for the authd package under test. +# authd-edge remains the default for local and normal CI runs. +PPA="${AUTHD_PPA:-ubuntu-enterprise-desktop/authd-edge}" $SSH "add-apt-repository -y ppa:${PPA}" # Configure authd to be verbose. We do this before installing authd to avoid diff --git a/e2e-tests/vm/provision.sh b/e2e-tests/vm/provision.sh index 1bbc565bd8..0b8ec3d3f2 100755 --- a/e2e-tests/vm/provision.sh +++ b/e2e-tests/vm/provision.sh @@ -7,13 +7,14 @@ CONFIG_FILE="${SCRIPT_DIR}/config.env" usage(){ cat << EOF -Usage: $0 [--config-file ] [--release ] [--broker ] [--authd-deb ] [--broker-snap ] [--force] +Usage: $0 [--config-file ] [--release ] [--broker ] [--authd-deb ] [--authd-ppa ] [--broker-snap ] [--force] Options: --config-file Path to the configuration file (default: config.env) --release Ubuntu release to provision (e.g. noble, resolute); overrides config file --broker The broker to install ("authd-google", "authd-msentraid", ...) --authd-deb Path to the authd deb file to install (default: install from the edge PPA) + --authd-ppa PPA to use instead of authd-edge when installing authd and its dependencies --broker-snap Path to the broker snap file to install (default: install from the edge channel) --force Force provisioning: remove existing VM and artifacts and create a fresh VM -h, --help Show this help message and exit @@ -45,6 +46,10 @@ while [[ $# -gt 0 ]]; do AUTHD_DEB="$2" shift 2 ;; + --authd-ppa) + AUTHD_PPA="$2" + shift 2 + ;; --broker-snap) BROKER_SNAP="$2" shift 2 @@ -78,5 +83,6 @@ set -x ${RELEASE_ARG:+--release "${RELEASE_ARG}"} \ ${BROKER:+--broker "${BROKER}"} \ ${AUTHD_DEB:+--authd-deb "${AUTHD_DEB}"} \ + ${AUTHD_PPA:+--authd-ppa "${AUTHD_PPA}"} \ ${BROKER_SNAP:+--broker-snap "${BROKER_SNAP}"} \ ${FORCE:+--force} diff --git a/pam/internal/adapter/authmodeselection.go b/pam/internal/adapter/authmodeselection.go index 2cfc208916..947497d5fb 100644 --- a/pam/internal/adapter/authmodeselection.go +++ b/pam/internal/adapter/authmodeselection.go @@ -37,7 +37,8 @@ type authModesReceived struct { // authModeSelected is the internal event signalling that the an authentication mode has been selected. type authModeSelected struct { - id string + id string + fromGDM bool } // selectAuthMode selects current authentication mode. @@ -49,6 +50,15 @@ func selectAuthMode(id string) tea.Cmd { } } +func selectGdmAuthMode(id string) tea.Cmd { + return func() tea.Msg { + return authModeSelected{ + id: id, + fromGDM: true, + } + } +} + // newAuthModeSelectionModel initializes an empty list with default options of authModeSelectionModel. func newAuthModeSelectionModel(clientType PamClientType) authModeSelectionModel { return authModeSelectionModel{ @@ -209,7 +219,8 @@ func (m authModeSelectionModel) Update(msg tea.Msg) (authModeSelectionModel, tea } return m, sendEvent(AuthModeSelected{ - ID: msg.id, + ID: msg.id, + fromGDM: msg.fromGDM, }) } diff --git a/pam/internal/adapter/gdmmodel.go b/pam/internal/adapter/gdmmodel.go index d9cbcefce1..5ff9255633 100644 --- a/pam/internal/adapter/gdmmodel.go +++ b/pam/internal/adapter/gdmmodel.go @@ -34,6 +34,14 @@ type gdmModel struct { // further conversation with GDM should happen. conversationsStopped bool stoppingConversations bool + + // pendingEchoAuthModeID is the auth mode we last told GDM to select and + // whose echo we still expect back. GDM echoes our selection in its next + // poll; acting on that echo would issue a second SelectAuthenticationMode + // RPC (and, for device auth, mint a second device code that orphans the + // in-flight poll). It is consumed (cleared) by the first matching echo, so + // a later genuine re-selection of the same mode is still honored. + pendingEchoAuthModeID string } type gdmPollResponse struct { @@ -111,7 +119,7 @@ func (m gdmModel) pollGdm() tea.Cmd { } } -func (m gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) tea.Cmd { +func (m gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) (gdmModel, tea.Cmd) { if log.IsLevelEnabled(log.DebugLevel) { for _, result := range gdmPollResults { log.Debugf(context.TODO(), "GDM poll response: %v", result.SafeString()) @@ -127,7 +135,7 @@ func (m gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) tea.Cmd { case *gdm.EventData_BrokerSelected: if res.BrokerSelected == nil { - return sendEvent(pamError{status: pam.ErrSystem, + return m, sendEvent(pamError{status: pam.ErrSystem, msg: "missing broker selected", }) } @@ -137,11 +145,24 @@ func (m gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) tea.Cmd { case *gdm.EventData_AuthModeSelected: if res.AuthModeSelected == nil { - return sendEvent(pamError{ + return m, sendEvent(pamError{ status: pam.ErrSystem, msg: "missing auth mode id", }) } - commands = append(commands, selectAuthMode(res.AuthModeSelected.AuthModeId)) + // GDM echoes back the auth mode we just told it to select. Ignore + // that one echo to avoid issuing a duplicate SelectAuthenticationMode + // RPC (which, for device auth, mints a second device code and + // orphans the in-flight poll). This is a one-shot per selection: + // a later genuine re-selection of the same mode (the user picking + // it again) is honored because the pending echo has been consumed. + if res.AuthModeSelected.AuthModeId == m.pendingEchoAuthModeID { + log.Debugf(context.TODO(), + "Ignoring GDM auth mode selection echo for %q", + res.AuthModeSelected.AuthModeId) + m.pendingEchoAuthModeID = "" + break + } + commands = append(commands, selectGdmAuthMode(res.AuthModeSelected.AuthModeId)) case *gdm.EventData_IsAuthenticatedRequested: if !m.waitingAuth { @@ -150,7 +171,7 @@ func (m gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) tea.Cmd { } m.waitingAuth = false if res.IsAuthenticatedRequested == nil || res.IsAuthenticatedRequested.AuthenticationData == nil { - return sendEvent(pamError{ + return m, sendEvent(pamError{ status: pam.ErrSystem, msg: "missing auth requested", }) } @@ -173,7 +194,7 @@ func (m gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) tea.Cmd { case *gdm.EventData_StageChanged: if res.StageChanged == nil { - return sendEvent(pamError{ + return m, sendEvent(pamError{ status: pam.ErrSystem, msg: "missing stage changed", }) } @@ -183,7 +204,7 @@ func (m gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) tea.Cmd { } commands = append(commands, sendEvent(gdmPollDone{})) - return tea.Sequence(commands...) + return m, tea.Sequence(commands...) } func (m gdmModel) emitEvent(event gdm.Event) tea.Cmd { @@ -211,7 +232,9 @@ func (m gdmModel) Update(msg tea.Msg) (gdmModel, tea.Cmd) { switch msg := msg.(type) { case gdmPollResponse: - return m, m.handlePollResponse(msg.pollResponse) + var cmd tea.Cmd + m, cmd = m.handlePollResponse(msg.pollResponse) + return m, cmd case gdmPollDone: return m, tea.Sequence( @@ -219,6 +242,10 @@ func (m gdmModel) Update(msg tea.Msg) (gdmModel, tea.Cmd) { m.pollGdm()) case StageChanged: + // A genuine (re-)selection always follows a stage change into the + // authModeSelection stage, so any echo we were still expecting from a + // previous selection is no longer relevant once the stage changes. + m.pendingEchoAuthModeID = "" return m, m.changeStage(msg.Stage) case userSelected: @@ -242,6 +269,13 @@ func (m gdmModel) Update(msg tea.Msg) (gdmModel, tea.Cmd) { }) case AuthModeSelected: + if !msg.fromGDM { + // Only selections sent to GDM are echoed in a later poll. A + // selection received from GDM is already that echo (or a genuine + // user re-selection), so recording it would suppress the next + // selection of the same mode. + m.pendingEchoAuthModeID = msg.ID + } return m, m.emitEvent(&gdm.EventData_AuthModeSelected{ AuthModeSelected: &gdm.Events_AuthModeSelected{AuthModeId: msg.ID}, }) diff --git a/pam/internal/adapter/gdmmodel_authmode_echo_test.go b/pam/internal/adapter/gdmmodel_authmode_echo_test.go new file mode 100644 index 0000000000..474a8cca7a --- /dev/null +++ b/pam/internal/adapter/gdmmodel_authmode_echo_test.go @@ -0,0 +1,141 @@ +package adapter + +import ( + "reflect" + "testing" + + "github.com/canonical/authd/pam/internal/gdm" + "github.com/canonical/authd/pam/internal/gdm_test" + "github.com/canonical/authd/pam/internal/proto" + tea "github.com/charmbracelet/bubbletea" + "github.com/stretchr/testify/require" +) + +// collectMessages runs a command and recursively flattens the batch/sequence +// messages it produces into the concrete messages they ultimately deliver. +// tea.Batch and tea.Sequence return []tea.Cmd-shaped messages whose concrete +// types are unexported, so they are detected structurally via reflection. +func collectMessages(cmd tea.Cmd) []tea.Msg { + if cmd == nil { + return nil + } + msg := cmd() + if cmds, ok := asCmdSlice(msg); ok { + var msgs []tea.Msg + for _, c := range cmds { + msgs = append(msgs, collectMessages(c)...) + } + return msgs + } + return []tea.Msg{msg} +} + +// asCmdSlice reports whether msg is a []tea.Cmd-shaped batch/sequence message +// and, if so, returns its commands. +func asCmdSlice(msg tea.Msg) ([]tea.Cmd, bool) { + v := reflect.ValueOf(msg) + if v.Kind() != reflect.Slice || v.Type().Elem() != reflect.TypeOf(tea.Cmd(nil)) { + return nil, false + } + cmdType := reflect.TypeOf(tea.Cmd(nil)) + cmds := make([]tea.Cmd, v.Len()) + for i := range cmds { + cmd, ok := v.Index(i).Convert(cmdType).Interface().(tea.Cmd) + if !ok { + return nil, false + } + cmds[i] = cmd + } + return cmds, true +} + +func containsAuthModeSelected(msgs []tea.Msg, id string) bool { + for _, msg := range msgs { + if m, ok := msg.(authModeSelected); ok && m.id == id { + return true + } + } + return false +} + +func TestGdmModelIgnoresAuthModeSelectedEcho(t *testing.T) { + t.Parallel() + + // After we select an auth mode, GDM echoes the selection back as a poll + // event. Acting on that echo would re-run SelectAuthenticationMode and, for + // device auth, mint a second device code while the poll is still on the + // first one (UDENG-8799). + m := gdmModel{} + m, _ = m.Update(AuthModeSelected{ID: "device_auth_qr"}) + require.Equal(t, "device_auth_qr", m.pendingEchoAuthModeID, + "selecting an auth mode should record the expected echo") + + echo := []*gdm.EventData{gdm_test.AuthModeSelectedEvent("device_auth_qr")} + var cmd tea.Cmd + m, cmd = m.handlePollResponse(echo) + msgs := collectMessages(cmd) + require.False(t, containsAuthModeSelected(msgs, "device_auth_qr"), + "echo of the just-selected auth mode must not trigger a re-selection") + require.Empty(t, m.pendingEchoAuthModeID, + "consuming the echo should clear the expected echo") +} + +func TestGdmModelActsOnAuthModeChange(t *testing.T) { + t.Parallel() + + // A genuine change to a different auth mode must still be acted on. + m := gdmModel{} + m, _ = m.Update(AuthModeSelected{ID: "device_auth_qr"}) + + change := []*gdm.EventData{gdm_test.AuthModeSelectedEvent("password")} + _, cmd := m.handlePollResponse(change) + msgs := collectMessages(cmd) + require.True(t, containsAuthModeSelected(msgs, "password"), + "selecting a different auth mode must trigger a re-selection") +} + +func TestGdmModelActsOnSameAuthModeReselection(t *testing.T) { + t.Parallel() + + // Suppression is a one-shot: only the immediate echo of our own selection + // is dropped. A later genuine re-selection of the same auth mode (the user + // picking it again) must be honored, because the pending echo has already + // been consumed. + m := gdmModel{} + m, _ = m.Update(AuthModeSelected{ID: "device_auth_qr"}) + + echo := []*gdm.EventData{gdm_test.AuthModeSelectedEvent("device_auth_qr")} + m, cmd := m.handlePollResponse(echo) + _ = collectMessages(cmd) + require.Empty(t, m.pendingEchoAuthModeID, + "the echo should have been consumed") + + reselect := []*gdm.EventData{gdm_test.AuthModeSelectedEvent("device_auth_qr")} + _, cmd = m.handlePollResponse(reselect) + msgs := collectMessages(cmd) + require.True(t, containsAuthModeSelected(msgs, "device_auth_qr"), + "a genuine re-selection of the same auth mode must be honored") +} + +func TestGdmModelStageChangeClearsPendingEcho(t *testing.T) { + t.Parallel() + + // A genuine re-selection always follows a stage change back into + // authModeSelection. The stage change must drop any echo we were still + // expecting, so that the re-selection is acted on instead of being + // mistaken for the (never-delivered) echo of the previous selection. + m := gdmModel{} + m, _ = m.Update(AuthModeSelected{ID: "device_auth_qr"}) + require.Equal(t, "device_auth_qr", m.pendingEchoAuthModeID) + + m, _ = m.Update(StageChanged{Stage: proto.Stage_challenge}) + m, _ = m.Update(StageChanged{Stage: proto.Stage_authModeSelection}) + require.Empty(t, m.pendingEchoAuthModeID, + "a stage change must drop a still-pending echo") + + reselect := []*gdm.EventData{gdm_test.AuthModeSelectedEvent("device_auth_qr")} + _, cmd := m.handlePollResponse(reselect) + msgs := collectMessages(cmd) + require.True(t, containsAuthModeSelected(msgs, "device_auth_qr"), + "re-selecting the same auth mode after a stage change must be honored") +} diff --git a/pam/internal/adapter/gdmmodel_test.go b/pam/internal/adapter/gdmmodel_test.go index d7d88c8419..5ac30c5422 100644 --- a/pam/internal/adapter/gdmmodel_test.go +++ b/pam/internal/adapter/gdmmodel_test.go @@ -105,6 +105,7 @@ func TestGdmModel(t *testing.T) { wantPAMReturnValue PamReturnValue wantGdmRequests []gdm.RequestType wantGdmEvents []gdm.EventType + wantGdmEventsCount map[gdm.EventType]int wantGdmAuthRes []*authd.IAResponse wantNoGdmRequests []gdm.RequestType wantNoGdmEvents []gdm.EventType @@ -706,11 +707,16 @@ func TestGdmModel(t *testing.T) { gdm.EventType_authModesReceived, gdm.EventType_authModeSelected, gdm.EventType_uiLayoutReceived, - gdm.EventType_authModeSelected, - gdm.EventType_uiLayoutReceived, gdm.EventType_authEvent, // retry gdm.EventType_startAuthentication, }, + // One authModeSelected/uiLayoutReceived per genuine selection (the + // three password-stage cycles in wantGdmRequests). The GDM echo of + // each selection must not add extra cycles. + wantGdmEventsCount: map[gdm.EventType]int{ + gdm.EventType_authModeSelected: 3, + gdm.EventType_uiLayoutReceived: 3, + }, wantStage: proto.Stage_challenge, wantGdmAuthRes: []*authd.IAResponse{ { @@ -1290,6 +1296,15 @@ func TestGdmModel(t *testing.T) { gdm.EventType_startAuthentication, gdm.EventType_authEvent, }, + // Each genuine selection of the auth mode (the initial one and the + // re-selection after navigating back to authModeSelection) must + // produce exactly one selection cycle: the GDM echo of the + // selection must not add a third one. + wantGdmEventsCount: map[gdm.EventType]int{ + gdm.EventType_authModeSelected: 2, + gdm.EventType_uiLayoutReceived: 2, + gdm.EventType_startAuthentication: 2, + }, wantStage: proto.Stage_challenge, wantGdmAuthRes: []*authd.IAResponse{ {Access: auth.Granted}, @@ -2742,6 +2757,17 @@ func TestGdmModel(t *testing.T) { "Required events have not been received: %v vs %v", stringifySlice(tc.wantGdmEvents), stringifySlice(receivedEventTypes)) + for evType, wantN := range tc.wantGdmEventsCount { + gotN := 0 + for _, e := range receivedEventTypes { + if e == evType { + gotN++ + } + } + require.Equal(t, wantN, gotN, + "GDM event %q received %d times, want %d", evType, gotN, wantN) + } + require.Empty(t, appState.wantMessages, "Wanted messages have not all been processed") username, err := appState.pamMTx.GetItem(pam.User) diff --git a/pam/internal/adapter/model.go b/pam/internal/adapter/model.go index 6225a4c9fe..4d3b7c038a 100644 --- a/pam/internal/adapter/model.go +++ b/pam/internal/adapter/model.go @@ -108,7 +108,8 @@ type GetAuthenticationModesRequested struct{} // AuthModeSelected is triggered when the authentication mode has been chosen. type AuthModeSelected struct { - ID string + ID string + fromGDM bool } // UILayoutReceived means that we got the ui layout to display by the broker.