diff --git a/components/2.0/PasswordBox.qml b/components/2.0/PasswordBox.qml index 8973eb2..76ddd6c 100644 --- a/components/2.0/PasswordBox.qml +++ b/components/2.0/PasswordBox.qml @@ -37,6 +37,8 @@ FocusScope { property alias textColor: txtMain.textColor property alias echoMode: txtMain.echoMode property alias text: txtMain.text + property alias validator: txtMain.validator + property alias maximumLength: txtMain.maximumLength property alias image: img.source property double imageFadeIn: 300 @@ -47,6 +49,8 @@ FocusScope { property alias tooltipFG: tooltipText.color property alias tooltipBG: tooltip.color + signal accepted + TextConstants { id: textConstants } @@ -57,8 +61,12 @@ FocusScope { font.pixelSize: 14 echoMode: TextInput.Password + // default validator + validator: RegExpValidator { regExp: /[][!"§#$%&'()*+,./:;<=>?@\^_´`{|}~a-zA-Z0-9-]*/ } focus: true + + onAccepted: container.accepted() } Image { diff --git a/components/2.0/PasswordConnections.qml b/components/2.0/PasswordConnections.qml new file mode 100644 index 0000000..d38d8b3 --- /dev/null +++ b/components/2.0/PasswordConnections.qml @@ -0,0 +1,120 @@ +/*************************************************************************** +* Copyright (c) 2017 Thomas Hoehn +* +* Permission is hereby granted, free of charge, to any person +* obtaining a copy of this software and associated documentation +* files (the "Software"), to deal in the Software without restriction, +* including without limitation the rights to use, copy, modify, merge, +* publish, distribute, sublicense, and/or sell copies of the Software, +* and to permit persons to whom the Software is furnished to do so, +* subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included +* in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +* OR OTHER DEALINGS IN THE SOFTWARE. +* +* Note: container for password renewal logic +* +***************************************************************************/ + +import QtQuick 2.0 +import SddmComponents 2.0 + +Item { + + property var pwdItem + property var renewalDialog + + // if provided supposed to have text property + property var errMsg + property var txtMsg + + // gets focus when password renewal dialog closes + property var getsBackFocus + + TextConstants { id: textConstants } + + function clearPwd() { + if(typeof pwdItem.password !== "undefined") + pwdItem.password = "" + else if(typeof pwdItem.text !== "undefined") + pwdItem.text = "" + } + + // handles password renewal events + Connections { + + target: renewalDialog + + onOk: { + sddm.pamResponse(renewalDialog.password) + } + + onCancel: { + sddm.cancelPamConv() + } + + onError: { + // unlikely: invalid prompt, + // i.e. new/repeat prompt for dialog not found in request + if(typeof errMsg !== "undefined") + errMsg.text = msg + } + + onVisibleChanged: { + renewalDialogTimer.stop() + + if(!renewalDialog.visible) { + renewalDialog.clear() // clear pam infos + clearPwd() + + // last selected user or input field gets focus back + if(typeof getsBackFocus !== "undefined") + getsBackFocus.forceActiveFocus() + } + } + } + + Connections { + + target: sddm + + onLoginSucceeded: { + // ...will not be reached as greeter stops after successfull login... + } + + onLoginFailed: { + if(typeof txtMsg !== "undefined") + { + txtMsg.text = textConstants.loginFailed + // filter out login failure details + if(err_msg != "Authentication failure" && + err_msg != "Password change aborted." && + err_msg != "Authentication token manipulation error") + txtMsg.text += "\n" + err_msg + } + renewalDialog.visible = false + clearPwd() + } + + // show messages from pam conversation (for expired passwords) + onPamConvMsg: { + // from signal pamConvMsg(pam_msg) + renewalDialog.append(pam_msg) + } + + // new pam request arrived, e.g. for expired password, + onPamRequest: { + // open password renewalDialog dialog and block other GUI + renewalDialog.show(request.findNewPwdMessage(), + request.findRepeatPwdMessage()) + } + } +} diff --git a/components/2.0/PasswordRenewal.qml b/components/2.0/PasswordRenewal.qml new file mode 100644 index 0000000..72c4890 --- /dev/null +++ b/components/2.0/PasswordRenewal.qml @@ -0,0 +1,276 @@ +/*************************************************************************** +* Copyright (c) 2017 Thomas Hoehn +* +* Permission is hereby granted, free of charge, to any person +* obtaining a copy of this software and associated documentation +* files (the "Software"), to deal in the Software without restriction, +* including without limitation the rights to use, copy, modify, merge, +* publish, distribute, sublicense, and/or sell copies of the Software, +* and to permit persons to whom the Software is furnished to do so, +* subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included +* in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +* OR OTHER DEALINGS IN THE SOFTWARE. +* +* Note: simple password input dialog for user with expired password (pam conversation) +* +***************************************************************************/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +FocusScope { + id: container + implicitHeight: dialogHeight + implicitWidth: infosWidth+2*margins + + readonly property string password: pwd1Input.text + + // prompt messages from pam + property alias prompt1: prompt1Txt.text + property alias prompt2: prompt2Txt.text + + // dialog window + property int dialogHeight: 180 + property int margins: 8 + + // infos text area with pam messages + property int infosWidth: 520 + property int infosHeight: 10 + + property color promptColor: "black" + property color infosColor: "red" + property alias color: dialog.color // dialog background + property alias radius: dialog.radius + property alias border: dialog.border + + // error texts + property string noMatchWarning: qsTr("Passwords do not match!") + property string samePwdWarning: qsTr("Password same as before. Please change!") + property string pwdEmptyWarning: qsTr("Empty password. Please enter password!") + + signal ok // password input completed + signal cancel // dialog canceled + signal error(string msg) // invalid prompts + + // set prompts and show dialog + function show(newPwdTxt, repeatPwdTxt) { + if(newPwdTxt != "" && repeatPwdTxt != "") + { + prompt1Txt.text = newPwdTxt + prompt2Txt.text = repeatPwdTxt + container.visible = true + } else { + // todo: prompts unknown (new/repeat password) + error("unknown pam message prompts!\n" + + "prompt1=" + newPwdTxt + + "prompt2=" + repeatPwdTxt) + } + } + + // clear all text + function clear() { + infoarea.text = "" + pwd1Input.text = "" + pwd2Input.text = "" + } + + // append new pam error/info + function append(msg) { + infoarea.append(msg) + } + + // internal + + function resetEmptyWarning() { + emptyWarningShown = false + } + + // flag: warning about empty password already shown? + property bool emptyWarningShown: false + // previously typed password (refused by pam) + property string previousPwd: "" + + function validatePwds() { + if(pwd1Input.text.length == 0 || + pwd2Input.text.length == 0) + { + if(emptyWarningShown==false) + { + infoarea.append(pwdEmptyWarning) + emptyWarningShown = true + } + return + } + + if(pwd1Input.text.length>0) + { + resetEmptyWarning() + + if(pwd1Input.text != pwd2Input.text) { + infoarea.append(noMatchWarning) + return + } + + if(previousPwd == pwd1Input.text) { + // if pam refuses new password (e.g. too similar) + // and this is presented to pam several times pam + // seems pissed off and stops conversation, so avoid + infoarea.append(samePwdWarning) + return + } + + // new password ok + previousPwd = pwd1Input.text + ok() + } + } + + onVisibleChanged: { + if(visible == true) + pwd1Input.text = "" + pwd2Input.text = "" + pwd1Input.focus = true + focus = true + } + + Rectangle { + id: dialog + anchors.fill: parent + + GridLayout { + id: contentGrid + anchors.fill: parent + columns: 2 + + Item { + width: infosWidth + height: infosHeight+36 + Layout.columnSpan: 2 + Layout.minimumWidth: infosWidth + Layout.margins: margins + + // show info/error from pam conv() + TextArea { + id: infoarea + anchors.fill: parent + viewport.implicitHeight: infosHeight + backgroundVisible: false + activeFocusOnPress: false + readOnly: true + textColor: infosColor + frameVisible: false + verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff + } + } + + // shows message (prompt) from pam conv() + Text { + id: prompt1Txt + Layout.minimumWidth: 64 + Layout.alignment: Qt.AlignRight + color: promptColor + text: qsTr("New password:") + } + + PasswordBox { + id: pwd1Input + implicitWidth: 150 + maximumLength: 64 + KeyNavigation.down: pwd2Input + KeyNavigation.tab: pwd2Input + Keys.onEnterPressed:pwd2Input.forceActiveFocus() + Keys.onReturnPressed: pwd2Input.forceActiveFocus() + onAccepted: { + if(pwd1Input.text != "" && pwd2Input.text != "" ) + validatePwds() + } + } + + // shows message (prompt) from pam conv() + Text { + id: prompt2Txt + Layout.minimumWidth: 64 + Layout.alignment: Qt.AlignRight + color: promptColor + text: qsTr("Repeat password:") + } + + PasswordBox { + id: pwd2Input + implicitWidth: 150 + maximumLength: 64 + KeyNavigation.down: cancelBtn + KeyNavigation.tab: cancelBtn + Keys.onEnterPressed: if(pwd1Input == "") pwd1Input.forceActiveFocus() + Keys.onReturnPressed: if(pwd1Input == "") pwd1Input.forceActiveFocus() + onAccepted: validatePwds() + } + + RowLayout { + + Layout.columnSpan: 2 + Layout.bottomMargin: 32 + Layout.topMargin: margins + Layout.fillWidth: true + Layout.alignment: Qt.AlignCenter + spacing: 8 + + Button { + id: cancelBtn + text: qsTr("Cancel") + activeFocusOnTab : true + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + KeyNavigation.right: resetBtn + onClicked: { + container.visible = false + resetEmptyWarning() + cancel() + } + } + + Button { + id: resetBtn + text: qsTr("Reset") + activeFocusOnTab : true + enabled: pwd1Input.text != "" || pwd2Input.text != "" + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + KeyNavigation.right: okBtn + KeyNavigation.left: cancelBtn + KeyNavigation.up: pwd2Input + onClicked: { + pwd1Input.text = "" + pwd2Input.text = "" + resetEmptyWarning() + pwd1Input.forceActiveFocus() + } + } + + Button { + id: okBtn + text: qsTr("Ok") + activeFocusOnTab : true + enabled: pwd1Input.text != "" && pwd2Input.text != "" + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + KeyNavigation.left: resetBtn + KeyNavigation.tab: pwd1Input + KeyNavigation.up: pwd2Input + onClicked: validatePwds() + } + } + } + } +} diff --git a/components/2.0/TextBox.qml b/components/2.0/TextBox.qml index a8d89af..c452738 100644 --- a/components/2.0/TextBox.qml +++ b/components/2.0/TextBox.qml @@ -37,6 +37,10 @@ FocusScope { property alias textColor: txtMain.color property alias echoMode: txtMain.echoMode property alias text: txtMain.text + property alias validator: txtMain.validator + property alias maximumLength: txtMain.maximumLength + + signal accepted Rectangle { id: main @@ -85,5 +89,7 @@ FocusScope { focus: true passwordCharacter: "\u25cf" + + onAccepted: container.accepted() } } --- a/components/common/qmldir +++ b/components/common/qmldir @@ -8,6 +8,8 @@ ImageButton 2.0 ImageButton.qml LayoutBox 2.0 LayoutBox.qml Menu 2.0 Menu.qml PasswordBox 2.0 PasswordBox.qml +PasswordRenewal 2.0 PasswordRenewal.qml +PasswordConnections 2.0 PasswordConnections.qml PictureBox 2.0 PictureBox.qml TextBox 2.0 TextBox.qml TextConstants 2.0 TextConstants.qml diff --git a/data/themes/elarun/Main.qml b/data/themes/elarun/Main.qml index 37a97a0..a943e87 100644 --- a/data/themes/elarun/Main.qml +++ b/data/themes/elarun/Main.qml @@ -35,13 +35,13 @@ Rectangle { TextConstants { id: textConstants } - Connections { - target: sddm - onLoginSucceeded: { - } - onLoginFailed: { - pw_entry.text = "" - } + // container for password renewal logic + PasswordConnections { + renewalDialog: renewal + pwdItem: pw_entry // use PasswordBox.text + getsBackFocus: pw_entry + //errMsg: none + //txtMsg: none } Background { @@ -60,9 +60,22 @@ Rectangle { color: "transparent" //visible: primaryScreen + PasswordRenewal { + id: renewal + anchors.horizontalCenter: rectangle.horizontalCenter + anchors.top: rectangle.bottom + anchors.topMargin: 32 + visible: false + radius: 8 + color: "#22888888" + infosColor: "lightcoral" + } + Rectangle { + id: rectangle width: 416; height: 262 color: "#00000000" + enabled: !renewal.visible anchors.centerIn: parent diff --git a/data/themes/maldives/Main.qml b/data/themes/maldives/Main.qml index 00feea4..286ad80 100644 --- a/data/themes/maldives/Main.qml +++ b/data/themes/maldives/Main.qml @@ -35,19 +35,13 @@ Rectangle { TextConstants { id: textConstants } - Connections { - target: sddm - - onLoginSucceeded: { - errorMessage.color = "steelblue" - errorMessage.text = textConstants.loginSucceeded - } - - onLoginFailed: { - password.text = "" - errorMessage.color = "red" - errorMessage.text = textConstants.loginFailed - } + // container for password renewal logic + PasswordConnections { + renewalDialog: renewal + pwdItem: password // use PasswordBox.text + getsBackFocus: password + errMsg: errorMessage + txtMsg: errorMessage } Background { @@ -76,10 +70,31 @@ Rectangle { } Image { + id: renewalImg + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: 64 + width: renewal.width+32 + height: renewal.height+32 + visible: renewal.visible + + source: "rectangle.png" + } + + PasswordRenewal { + id: renewal + anchors.centerIn: renewalImg + visible: false + radius: 8 + color: "transparent" + } + + Image { id: rectangle anchors.centerIn: parent width: Math.max(320, mainColumn.implicitWidth + 50) height: Math.max(320, mainColumn.implicitHeight + 50) + enabled: !renewal.visible source: "rectangle.png" diff --git a/docs/EXPIRED-PASSWORD.md b/docs/EXPIRED-PASSWORD.md new file mode 100755 index 0000000..e988488 --- /dev/null +++ b/docs/EXPIRED-PASSWORD.md @@ -0,0 +1,83 @@ +## Introduction + +SDDM can handle expired passwords during login. +A password renewal dialog is provided to change the expired password. +The greeter frontend will talk with the backend to handle the pam conversation. +Support for password renewal is provided with two components used in greeter themes: + +* ``PasswordRenewal.qml`` basic dialog for password renewal, for password input and confirmation +* ``PasswordConnections.qml`` container for Connections, hides signal handling from themes + +## Usage + +For new themes the two components have to be included in the ``Main.qml``. +See the built-in greeter theme ``Main.qml`` for an example, +and add this to ``Main.qml``: + +``` +// container with password renewal logic +PasswordConnections { + renewalDialog: renewal // PasswordRenewal dialog id (see below) + pwdItem: listView.currentItem // gets password input from currentItem.password + getsBackFocus: listView // item where focus falls back after dialog closes + errMsg: errMessage // if set defines (text) item which shows errors + txtMsg: txtMessage // if set (text) item which shows pam infos +} + +... + + Item { + id: usersContainer + // block other input during password renewal + enabled: !renewal.visible + +... + + // password renewal dialog + PasswordRenewal { + id: renewal + // customize here e.g.: + //anchors.horizontalCenter: parent.horizontalCenter + //anchors.bottom: usersContainer.top + //visible: false + //color: "#22888888" + //promptColor: "white" + //infosColor: "lightcoral" + } +``` + +## Some more details + +The following qml objects are available in ``Main.qml``, +they are used for password renewal and pam conversation: + +### Signals + +Signals coming from daemon backend: + +* ``pamConvMsg(pam_msg)`` +Provides infos/errors from (pam) backend conversation to present to user. + +* ``pamRequest()`` +New request from (pam) backend, user response is required. + +### Property + +* ``request`` (type AuthRequest) +Prompts from (pam) backend with messages from pam_conv. + +### Response + +Responses from greeter frontend to the backend: + +* ``sddm.enablePwdRenewal()`` +The theme tells the greeter it can handle password renewal (has a dialog and logic as described above). +For themes which do not have a password renewal dialog, this method is not called. +In that case the pam conversation is just canceled (otherwise pam_conv user session sits there waiting for password response). +This will keep compatibility to (older) themes which do not support expired passwords yet. + +* ``sddm.pamResponse(password)`` +Send password response to (pam) backend, i.e. pam_conv. + +* ``sddm.cancelPamConv()`` +Cancel pam conversation with pam_conv. diff --git a/src/auth/Auth.cpp b/src/auth/Auth.cpp index d5dbca5..9bf3bb9 100644 --- a/src/auth/Auth.cpp +++ b/src/auth/Auth.cpp @@ -58,6 +58,7 @@ namespace SDDM { void dataPending(); void childExited(int exitCode, QProcess::ExitStatus exitStatus); void childError(QProcess::ProcessError error); + void cancelPamConv(); void requestFinished(); public: AuthRequest *request { nullptr }; @@ -133,6 +134,7 @@ namespace SDDM { child->setProcessEnvironment(env); connect(child, QOverload::of(&QProcess::finished), this, &Auth::Private::childExited); connect(child, QOverload::of(&QProcess::error), this, &Auth::Private::childError); + connect(request, &AuthRequest::canceled, this, &Auth::Private::cancelPamConv); connect(request, &AuthRequest::finished, this, &Auth::Private::requestFinished); connect(request, &AuthRequest::promptsChanged, parent, &Auth::requestChanged); } @@ -148,61 +150,67 @@ namespace SDDM { connect(socket, &QLocalSocket::readyRead, this, &Auth::Private::dataPending); } + // from (PAM) Backend to daemon void Auth::Private::dataPending() { Auth *auth = qobject_cast(parent()); Msg m = MSG_UNKNOWN; SafeDataStream str(socket); - str.receive(); - str >> m; - switch (m) { - case ERROR: { - QString message; - Error type = ERROR_NONE; - str >> message >> type; - Q_EMIT auth->error(message, type); - break; - } - case INFO: { - QString message; - Info type = INFO_NONE; - str >> message >> type; - Q_EMIT auth->info(message, type); - break; - } - case REQUEST: { - Request r; - str >> r; - request->setRequest(&r); - break; - } - case AUTHENTICATED: { - QString user; - str >> user; - if (!user.isEmpty()) { - auth->setUser(user); - Q_EMIT auth->authentication(user, true); + do { + str.receive(); + str >> m; + switch (m) { + case ERROR: { + QString message; + Error type = ERROR_NONE; + str >> message >> type; + Q_EMIT auth->error(message, type); + break; + } + case INFO: { + QString message; + Info type = INFO_NONE; + str >> message >> type; + Q_EMIT auth->info(message, type); + break; + } + // request from (PAM) Backend + case REQUEST: { + Request r; + str >> r; + request->setRequest(&r); + qDebug() << "Auth: Request received"; + break; + } + case AUTHENTICATED: { + QString user; + str >> user; + if (!user.isEmpty()) { + auth->setUser(user); + Q_EMIT auth->authentication(user, true); + str.reset(); + str << AUTHENTICATED << environment << cookie; + str.send(); + } + else { + Q_EMIT auth->authentication(user, false); + } + break; + } + case SESSION_STATUS: { + bool status; + str >> status; + Q_EMIT auth->sessionStarted(status); str.reset(); - str << AUTHENTICATED << environment << cookie; + str << SESSION_STATUS; str.send(); + break; } - else { - Q_EMIT auth->authentication(user, false); + default: { + qWarning("Auth: dataPending: Unknown message received: %d", m); + Q_EMIT auth->error(QStringLiteral("Auth: Unexpected value received: %1").arg(m), ERROR_INTERNAL); } - break; - } - case SESSION_STATUS: { - bool status; - str >> status; - Q_EMIT auth->sessionStarted(status); - str.reset(); - str << SESSION_STATUS; - str.send(); - break; } - default: { - Q_EMIT auth->error(QStringLiteral("Auth: Unexpected value received: %1").arg(m), ERROR_INTERNAL); - } - } + } while(str.available()); } void Auth::Private::childExited(int exitCode, QProcess::ExitStatus exitStatus) { @@ -224,6 +232,16 @@ namespace SDDM { Q_EMIT qobject_cast(parent())->error(child->errorString(), ERROR_INTERNAL); } + // from daemon to (PAM) backend + void Auth::Private::cancelPamConv() { + SafeDataStream str(socket); + qDebug() << "Auth: cancelPamConv, send CANCEL to backend"; + str << CANCEL; + str.send(); + request->setRequest(); + } + + // from daemon to (PAM) backend void Auth::Private::requestFinished() { SafeDataStream str(socket); Request r = request->request(); @@ -232,7 +250,6 @@ namespace SDDM { request->setRequest(); } - Auth::Auth(const QString &user, const QString &session, bool autologin, QObject *parent, bool verbose) : QObject(parent) , d(new Private(this)) { diff --git a/src/auth/Auth.h b/src/auth/Auth.h index 9c26b95..cb7a736 100644 --- a/src/auth/Auth.h +++ b/src/auth/Auth.h @@ -21,8 +21,8 @@ #ifndef SDDM_AUTH_H #define SDDM_AUTH_H -#include "AuthRequest.h" #include "AuthPrompt.h" +#include "AuthRequest.h" #include #include @@ -66,6 +66,7 @@ namespace SDDM { enum Info { INFO_NONE = 0, INFO_UNKNOWN, + INFO_PAM_CONV, INFO_PASS_CHANGE_REQUIRED, _INFO_LAST }; diff --git a/src/auth/AuthBase.h b/src/auth/AuthBase.h new file mode 100644 index 0000000..ca09864 --- /dev/null +++ b/src/auth/AuthBase.h @@ -0,0 +1,137 @@ +/* + * PAM request and message IDs shared between greeter, daemon, helper + * Copyright (C) 2013 Martin Bříza + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#ifndef AUTHBASE_H +#define AUTHBASE_H + +#include + +// AuthPrompt::Type +#include "AuthPrompt.h" + +namespace SDDM { + class Prompt { + public: + Prompt() { } + Prompt(AuthPrompt::Type type, QString message, bool hidden) + : type(type), message(message), hidden(hidden) { } + Prompt(const Prompt &o) + : type(o.type), response(o.response), message(o.message), hidden(o.hidden) { } + ~Prompt() { + clear(); + } + Prompt& operator=(const Prompt &o) { + type = o.type; + response = o.response; + message = o.message; + hidden = o.hidden; + return *this; + } + bool operator==(const Prompt &o) const { + return type == o.type && response == o.response && message == o.message && hidden == o.hidden; + } + bool valid() const { + return !(type == AuthPrompt::NONE && response.isEmpty() && message.isEmpty()); + } + void clear() { + type = AuthPrompt::NONE; + // overwrite the whole thing with zeroes before clearing + memset(response.data(), 0, response.length()); + response.clear(); + message.clear(); + hidden = false; + } + + AuthPrompt::Type type { AuthPrompt::NONE }; + QByteArray response { }; + QString message { }; + bool hidden { false }; + }; + + class Request { + public: + Request() { } + Request(QList prompts) + : prompts(prompts) { } + Request(const Request &o) + : prompts(o.prompts) { } + Request& operator=(const Request &o) { + prompts = QList(o.prompts); + return *this; + } + bool operator==(const Request &o) const { + return prompts == o.prompts; + } + bool valid() const { + return !(prompts.isEmpty()); + } + void clear() { + prompts.clear(); + } + + QList prompts { }; + }; + + inline QDataStream& operator<<(QDataStream &s, const Prompt &m) { + s << qint32(m.type) << m.message << m.hidden << m.response; + return s; + } + + inline QDataStream& operator>>(QDataStream &s, Prompt &m) { + qint32 type; + QString message; + bool hidden; + QByteArray response; + s >> type >> message >> hidden >> response; + m.type = AuthPrompt::Type(type); + m.message = message; + m.hidden = hidden; + m.response = response; + return s; + } + + inline QDataStream& operator<<(QDataStream &s, const Request &m) { + qint32 length = m.prompts.length(); + s << length; + for(const Prompt &p : qAsConst(m.prompts)) { + s << p; + } + return s; + } + + inline QDataStream& operator>>(QDataStream &s, Request &m) { + QList prompts; + qint32 length; + s >> length; + for (int i = 0; i < length; i++) { + Prompt p; + s >> p; + prompts << p; + } + if (prompts.length() != length) { + s.setStatus(QDataStream::ReadCorruptData); + return s; + } + m.prompts = prompts; + return s; + } +} + +#endif // AUTHBASE_H diff --git a/src/auth/AuthMessages.h b/src/auth/AuthMessages.h index ec16d00..00dd714 100644 --- a/src/auth/AuthMessages.h +++ b/src/auth/AuthMessages.h @@ -26,68 +26,10 @@ #include "Auth.h" -namespace SDDM { - class Prompt { - public: - Prompt() { } - Prompt(AuthPrompt::Type type, QString message, bool hidden) - : type(type), message(message), hidden(hidden) { } - Prompt(const Prompt &o) - : type(o.type), response(o.response), message(o.message), hidden(o.hidden) { } - ~Prompt() { - clear(); - } - Prompt& operator=(const Prompt &o) { - type = o.type; - response = o.response; - message = o.message; - hidden = o.hidden; - return *this; - } - bool operator==(const Prompt &o) const { - return type == o.type && response == o.response && message == o.message && hidden == o.hidden; - } - bool valid() const { - return !(type == AuthPrompt::NONE && response.isEmpty() && message.isEmpty()); - } - void clear() { - type = AuthPrompt::NONE; - // overwrite the whole thing with zeroes before clearing - memset(response.data(), 0, response.length()); - response.clear(); - message.clear(); - hidden = false; - } - - AuthPrompt::Type type { AuthPrompt::NONE }; - QByteArray response { }; - QString message { }; - bool hidden { false }; - }; +// Request and Prompt classes +#include "AuthBase.h" - class Request { - public: - Request() { } - Request(QList prompts) - : prompts(prompts) { } - Request(const Request &o) - : prompts(o.prompts) { } - Request& operator=(const Request &o) { - prompts = QList(o.prompts); - return *this; - } - bool operator==(const Request &o) const { - return prompts == o.prompts; - } - bool valid() const { - return !(prompts.isEmpty()); - } - void clear() { - prompts.clear(); - } - - QList prompts { }; - }; +namespace SDDM { enum Msg { MSG_UNKNOWN = 0, @@ -95,6 +37,7 @@ namespace SDDM { ERROR, INFO, REQUEST, + CANCEL, AUTHENTICATED, SESSION_STATUS, MSG_LAST, @@ -166,49 +109,6 @@ namespace SDDM { return s; } - inline QDataStream& operator<<(QDataStream &s, const Prompt &m) { - s << qint32(m.type) << m.message << m.hidden << m.response; - return s; - } - - inline QDataStream& operator>>(QDataStream &s, Prompt &m) { - qint32 type; - QString message; - bool hidden; - QByteArray response; - s >> type >> message >> hidden >> response; - m.type = AuthPrompt::Type(type); - m.message = message; - m.hidden = hidden; - m.response = response; - return s; - } - - inline QDataStream& operator<<(QDataStream &s, const Request &m) { - qint32 length = m.prompts.length(); - s << length; - for(const Prompt &p : qAsConst(m.prompts)) { - s << p; - } - return s; - } - - inline QDataStream& operator>>(QDataStream &s, Request &m) { - QList prompts; - qint32 length; - s >> length; - for (int i = 0; i < length; i++) { - Prompt p; - s >> p; - prompts << p; - } - if (prompts.length() != length) { - s.setStatus(QDataStream::ReadCorruptData); - return s; - } - m.prompts = prompts; - return s; - } } #endif // MESSAGES_H diff --git a/src/auth/AuthPrompt.cpp b/src/auth/AuthPrompt.cpp index 7434ede..96b8034 100644 --- a/src/auth/AuthPrompt.cpp +++ b/src/auth/AuthPrompt.cpp @@ -19,8 +19,8 @@ */ #include "AuthPrompt.h" -#include "Auth.h" -#include "AuthMessages.h" +#include "AuthBase.h" +#include "AuthRequest.h" namespace SDDM { class AuthPrompt::Private : public Prompt { @@ -69,4 +69,37 @@ namespace SDDM { bool AuthPrompt::hidden() const { return d->hidden; } + + // @internal for debug logging purposes + const QString &AuthPrompt::typeToString(int type) { + + static const QString stringList[] = { + QStringLiteral("NONE"), + QStringLiteral("UNKNOWN"), + QStringLiteral("CHANGE_CURRENT"), + QStringLiteral("CHANGE_NEW"), + QStringLiteral("CHANGE_REPEAT"), + QStringLiteral("LOGIN_USER"), + QStringLiteral("LOGIN_PASSWORD"), + }; + + switch(type) { + case AuthPrompt::NONE: + return stringList[0]; break; + case AuthPrompt::CHANGE_CURRENT: + return stringList[2]; break; + case AuthPrompt::CHANGE_NEW: + return stringList[3]; break; + case AuthPrompt::CHANGE_REPEAT: + return stringList[4]; break; + case AuthPrompt::LOGIN_USER: + return stringList[5]; break; + case AuthPrompt::LOGIN_PASSWORD: + return stringList[6]; break; + default: break; + } + + // AuthPrompt::UNKNOWN + return stringList[1]; + } } diff --git a/src/auth/AuthPrompt.h b/src/auth/AuthPrompt.h index 8ed394d..2f1b0f0 100644 --- a/src/auth/AuthPrompt.h +++ b/src/auth/AuthPrompt.h @@ -24,7 +24,6 @@ #include namespace SDDM { - class Auth; class AuthRequest; class Prompt; /** @@ -85,6 +84,12 @@ namespace SDDM { * @param r data entered by the user */ void setResponse(const QByteArray &r); + /** + * @brief get string representation of AuthPrompt type, see enum @ref Type + * @note for debug logging + */ + static const QString &typeToString(int type); + Q_SIGNALS: /** * Emitted when the response was entered by the user @@ -99,4 +104,4 @@ namespace SDDM { }; } -#endif //PROMPT_H \ No newline at end of file +#endif //PROMPT_H diff --git a/src/auth/AuthRequest.cpp b/src/auth/AuthRequest.cpp index 2c4764a..0d82503 100644 --- a/src/auth/AuthRequest.cpp +++ b/src/auth/AuthRequest.cpp @@ -19,7 +19,6 @@ */ #include "AuthRequest.h" -#include "Auth.h" #include "AuthMessages.h" namespace SDDM { @@ -46,11 +45,11 @@ namespace SDDM { qobject_cast(parent())->done(); } - AuthRequest::AuthRequest(Auth *parent) + AuthRequest::AuthRequest(QObject *parent) : QObject(parent) , d(new Private(this)) { } - void AuthRequest::setRequest(const Request *request) { + void AuthRequest::setRequest(const Request * const request) { QList promptsCopy(d->prompts); d->prompts.clear(); if (request != nullptr) { @@ -72,7 +71,7 @@ namespace SDDM { return d->prompts; } - QQmlListProperty AuthRequest::promptsDecl() { + QQmlListProperty AuthRequest::promptsRead() { return QQmlListProperty(this, d->prompts); } @@ -83,6 +82,14 @@ namespace SDDM { } } + // send to (pam) backend + void AuthRequest::cancel() { + if (!d->finished) { + d->finished = true; + } + Q_EMIT canceled(); + } + bool AuthRequest::finishAutomatically() { return d->finishAutomatically; } @@ -106,6 +113,47 @@ namespace SDDM { } return r; } + + QString AuthRequest::findNewPwdMessage() { + for(const AuthPrompt *qap : qAsConst(d->prompts)) { + if(qap->type()==AuthPrompt::CHANGE_NEW) + return qap->message(); + } + return QString(); + } + + QString AuthRequest::findRepeatPwdMessage() { + for(const AuthPrompt *qap : qAsConst(d->prompts)) { + if(qap->type()==AuthPrompt::CHANGE_REPEAT) + return qap->message(); + } + return QString(); + } + + AuthPrompt *AuthRequest::findPrompt(AuthPrompt::Type type) const { + for(AuthPrompt *qap : d->prompts) { + if(qap->type()==type) + return qap; + } + return NULL; + } + + void AuthRequest::setChangeResponse(const QString ¤tPwd, const QString &newPassword) { + AuthPrompt* prompt; + + if(!currentPwd.isNull()) { + prompt = findPrompt(AuthPrompt::CHANGE_CURRENT); + if(prompt) prompt->setResponse(qPrintable(currentPwd)); + } + + if(!newPassword.isNull()) { + prompt = findPrompt(AuthPrompt::CHANGE_NEW); + if(prompt) prompt->setResponse(qPrintable(newPassword)); + + prompt = findPrompt(AuthPrompt::CHANGE_REPEAT); + if(prompt) prompt->setResponse(qPrintable(newPassword)); + } + } } #include "AuthRequest.moc" diff --git a/src/auth/AuthRequest.h b/src/auth/AuthRequest.h index b8ef43c..113fd10 100644 --- a/src/auth/AuthRequest.h +++ b/src/auth/AuthRequest.h @@ -23,11 +23,12 @@ #include +#include + #include namespace SDDM { - class Auth; - class AuthPrompt; + //class AuthPrompt; class Request; /** * \brief @@ -48,12 +49,28 @@ namespace SDDM { * * \todo Decide if it's sane to use the info messages from PAM or to somehow parse them * and make the password changing message into a Request::Type of some kind + * + * \note AuthRequest (and AuthPrompt) is mainly used in (Pam)Backend + * and closely bundled with the Auth.cpp parent which handles user session + * after authentication etc. + * + * But AuthRequest is also secondly used as pure (pam_conv) message container in greeter + * without the Auth parent overhead (but still with the signal/slot amenities) - depending on + * the constructor used. In this case it just hands over the pam_conv() messages and possibly + * user (password) responses between daemon, greeter and qml. */ class AuthRequest : public QObject { Q_OBJECT - Q_PROPERTY(QQmlListProperty prompts READ promptsDecl NOTIFY promptsChanged) + Q_PROPERTY(QQmlListProperty prompts READ promptsRead NOTIFY promptsChanged) Q_PROPERTY(bool finishAutomatically READ finishAutomatically WRITE setFinishAutomatically NOTIFY finishAutomaticallyChanged) + public: + /** @brief for AuthRequest without using Auth parent. + * + * when used as parameter container to hand over pam request (prompts with pam + * messages and user pwd responses) between daemon and greeter (proxy) to qml view. + */ + AuthRequest(QObject *parent = 0); /** * @return list of the contained prompts */ @@ -62,30 +79,68 @@ namespace SDDM { * For QML apps * @return list of the contained prompts */ - QQmlListProperty promptsDecl(); + QQmlListProperty promptsRead(); + /** + * @brief Find pam message of type CHANGE_NEW in prompts list + * @return pam message string + */ + Q_INVOKABLE QString findNewPwdMessage(); + /** + * @brief Find pam message of type CHANGE_REPEAT in prompts list + * @return pam message string + */ + Q_INVOKABLE QString findRepeatPwdMessage(); + /** + * @brief Write password responses into request for pam conv(), + * for AuthPrompt::CHANGE_NEW, CHANGE_REPEAT, CHANGE_CURRENT + * @param currentPwd Current user password (expired password) + * @param newPassword New user password (replaces expired password) + */ + Q_INVOKABLE void setChangeResponse(const QString ¤tPwd, const QString &newPassword); static AuthRequest *empty(); bool finishAutomatically(); + /** + * @brief Trigger slot done() automaticly when all AuthPrompt responses (user passwords) are set. + * @param value if true done() automaticly called, if false user calls done() after specifying all responses + */ void setFinishAutomatically(bool value); + /** + * @brief Fill AuthRequest with Prompts from Request + * @param Request Prompts with possible responses + */ + void setRequest(const Request * const request = nullptr); + /** + * @brief convert AuthRequest to simple Request type + * @return Request with Prompts + */ + Request request() const; + /** + * @brief find prompt with specified type + * @param type \ref AuthPrompt::Type + * @return pointer to prompt of that type + */ + AuthPrompt *findPrompt(AuthPrompt::Type type) const; + public Q_SLOTS: /** - * Call this slot when all prompts has been filled to your satisfaction + * Call this slot when all prompts has been filled (with responses) to your satisfaction */ void done(); + /** + * Call this slot when user canceled password dialog + */ + void cancel(); Q_SIGNALS: /** * Emitted when \ref done was called */ void finished(); - + void canceled(); void finishAutomaticallyChanged(); void promptsChanged(); - private: - AuthRequest(Auth *parent); - void setRequest(const Request *request = nullptr); - Request request() const; - friend class Auth; + protected: class Private; Private *d { nullptr }; }; diff --git a/src/common/Messages.h b/src/common/Messages.h index c779791..a587bb4 100644 --- a/src/common/Messages.h +++ b/src/common/Messages.h @@ -23,9 +23,12 @@ #include namespace SDDM { + /** daemon to greeter messages */ enum class GreeterMessages { Connect = 0, Login, + PamResponse, + PamCancel, PowerOff, Reboot, Suspend, @@ -33,9 +36,12 @@ namespace SDDM { HybridSleep }; + /** greeter to daemon messages */ enum class DaemonMessages { HostName, Capabilities, + PamConvMsg, + PamRequest, LoginSucceeded, LoginFailed }; diff --git a/src/common/SafeDataStream.cpp b/src/common/SafeDataStream.cpp index f62a074..332e6a0 100644 --- a/src/common/SafeDataStream.cpp +++ b/src/common/SafeDataStream.cpp @@ -74,6 +74,15 @@ namespace SDDM { } } + bool SafeDataStream::available() { + if (!m_device->isOpen()) { + qCritical() << " Auth: SafeDataStream: Could not read from the device"; + return false; + } + + return m_device->bytesAvailable(); + } + void SafeDataStream::reset() { m_data.clear(); device()->reset(); diff --git a/src/common/SafeDataStream.h b/src/common/SafeDataStream.h index 8bbbe8e..1f77920 100644 --- a/src/common/SafeDataStream.h +++ b/src/common/SafeDataStream.h @@ -29,6 +29,7 @@ namespace SDDM { SafeDataStream(QIODevice* device); void send(); void receive(); + bool available(); void reset(); private: diff --git a/src/common/SocketWriter.cpp b/src/common/SocketWriter.cpp index 32db235..3f8d0cf 100644 --- a/src/common/SocketWriter.cpp +++ b/src/common/SocketWriter.cpp @@ -49,4 +49,10 @@ namespace SDDM { return *this; } + + SocketWriter &SocketWriter::operator << (const Request &r) { + *output << r; + + return *this; + } } diff --git a/src/common/SocketWriter.h b/src/common/SocketWriter.h index 9b4036f..3c7a977 100644 --- a/src/common/SocketWriter.h +++ b/src/common/SocketWriter.h @@ -25,6 +25,7 @@ #include #include "Session.h" +#include "AuthBase.h" // for Request namespace SDDM { class SocketWriter { @@ -36,6 +37,7 @@ namespace SDDM { SocketWriter &operator << (const quint32 &u); SocketWriter &operator << (const QString &s); SocketWriter &operator << (const Session &s); + SocketWriter &operator << (const Request &r); private: QByteArray data; diff --git a/src/daemon/Display.cpp b/src/daemon/Display.cpp index ab84329..5066dd2 100644 --- a/src/daemon/Display.cpp +++ b/src/daemon/Display.cpp @@ -56,6 +56,8 @@ namespace SDDM { m_socketServer(new SocketServer(this)), m_greeter(new Greeter(this)) { + connect(m_greeter, SIGNAL(stopped()), this, SLOT(slotGreeterStopped())); + // respond to authentication requests m_auth->setVerbose(true); connect(m_auth, &Auth::requestChanged, this, &Display::slotRequestChanged); @@ -72,10 +74,17 @@ namespace SDDM { // connect login signal connect(m_socketServer, &SocketServer::login, this, &Display::login); + // pam responses and conversation cancel + connect(m_socketServer, SIGNAL(sendPamResponse(const QString&)), this, SLOT(setPamResponse(const QString&))); + connect(m_socketServer, SIGNAL(cancelPamConv()), this, SLOT(cancelPamConv())); // connect login result signals - connect(this, SIGNAL(loginFailed(QLocalSocket*)), m_socketServer, SLOT(loginFailed(QLocalSocket*))); + connect(this, SIGNAL(loginFailed(QLocalSocket*,const QString&)), m_socketServer, SLOT(loginFailed(QLocalSocket*,const QString&))); connect(this, SIGNAL(loginSucceeded(QLocalSocket*)), m_socketServer, SLOT(loginSucceeded(QLocalSocket*))); + // pam (conversation) info/error shown in greeter + connect(this, SIGNAL(pamConvMsg(QLocalSocket*,const QString&)), m_socketServer, SLOT(pamConvMsg(QLocalSocket*,const QString&))); + // new request from pam for e.g. password renewal (expired password) + connect(this, SIGNAL(pamRequest(QLocalSocket*,const AuthRequest * const)), m_socketServer, SLOT(pamRequest(QLocalSocket*,const AuthRequest * const))); } Display::~Display() { @@ -221,7 +230,9 @@ namespace SDDM { void Display::login(QLocalSocket *socket, const QString &user, const QString &password, const Session &session) { + m_socket = socket; + m_failed = false; //the SDDM user has special privileges that skip password checking so that we can load the greeter //block ever trying to log in as the SDDM user @@ -233,6 +244,20 @@ namespace SDDM { startAuth(user, password, session); } + // password renewal, got response from greeter + void Display::setPamResponse(const QString &password) { + qDebug() << "Display: set pam response with new password"; + m_auth->request()->setChangeResponse(m_passPhrase, password); + if(m_auth->request()->finishAutomatically() == false) + m_auth->request()->done(); + } + + // cancel pam (password renewal) conversation + // because user canceled password dialog in greeter + void Display::cancelPamConv() { + m_auth->request()->cancel(); + } + QString Display::findGreeterTheme() const { QString themeName = mainConfig.Theme.Current.get(); @@ -351,7 +376,7 @@ namespace SDDM { void Display::slotAuthenticationFinished(const QString &user, bool success) { if (success) { - qDebug() << "Authenticated successfully"; + qDebug() << "Authenticated successfully" << user; m_auth->setCookie(qobject_cast(m_displayServer)->cookie()); @@ -372,28 +397,54 @@ namespace SDDM { if (m_socket) emit loginSucceeded(m_socket); - } else if (m_socket) { + } else { qDebug() << "Authentication failure"; - emit loginFailed(m_socket); + + if (!m_socket) { + qWarning() << "Greeter socket down!"; + return; + } + + // avoid to emit loginFailed twice + if(!m_failed) { + emit loginFailed(m_socket, QString()); + m_failed = true; + } } - m_socket = nullptr; } + // presentable to the user void Display::slotAuthInfo(const QString &message, Auth::Info info) { - // TODO: presentable to the user, eventually - Q_UNUSED(info); qWarning() << "Authentication information:" << message; + + if (!m_socket) { + qWarning() << "Greeter socket down!"; + return; + } + + if(info == Auth::INFO_PASS_CHANGE_REQUIRED || + info == Auth::INFO_PAM_CONV) + { + // send pam conversation message to greeter + emit pamConvMsg(m_socket, message); + } } void Display::slotAuthError(const QString &message, Auth::Error error) { // TODO: handle more errors qWarning() << "Authentication error:" << message; - if (!m_socket) + if (!m_socket) { + qWarning() << "Greeter socket down!"; return; + } - if (error == Auth::ERROR_AUTHENTICATION) - emit loginFailed(m_socket); + // avoid to emit loginFailed twice + if (error == Auth::ERROR_AUTHENTICATION && !m_failed) + { + m_failed = true; + emit loginFailed(m_socket, message); + } } void Display::slotHelperFinished(Auth::HelperExitStatus status) { @@ -406,18 +457,62 @@ namespace SDDM { stop(); } + /* got new request (with prompts) from pam conv(), e.g. for expired pwd + * which requires response from greeter UI (new password) */ void Display::slotRequestChanged() { - if (m_auth->request()->prompts().length() == 1) { + + int n_prompts = m_auth->request()->prompts().count(); + AuthPrompt::Type type_prompt0(AuthPrompt::NONE); + + // get type if we have a prompt + if(n_prompts>0) + type_prompt0 = m_auth->request()->prompts()[0]->type(); + // ignore empty requests + else return; + + // see what we got from pam conv() and will be send to greeter + qDebug() << "Display: requestChanged with " << n_prompts << " prompts from Auth"; + + // respond to login password request + if (n_prompts == 1 && type_prompt0 == AuthPrompt::LOGIN_PASSWORD) { m_auth->request()->prompts()[0]->setResponse(qPrintable(m_passPhrase)); m_auth->request()->done(); - } else if (m_auth->request()->prompts().length() == 2) { + return; + } + + // handle request with user name and password prompts + if (n_prompts == 2 && type_prompt0 == AuthPrompt::LOGIN_USER ) + { m_auth->request()->prompts()[0]->setResponse(qPrintable(m_auth->user())); m_auth->request()->prompts()[1]->setResponse(qPrintable(m_passPhrase)); m_auth->request()->done(); + return; + } + + // handle password renewal case (gets response from greeter), + // will finish with request->done() later in setPamResponse() + if (n_prompts >= 2 && + m_auth->request()->findPrompt(AuthPrompt::CHANGE_NEW) && + m_auth->request()->findPrompt(AuthPrompt::CHANGE_REPEAT)) + { + if(m_socket) + // send password renewal request to greeter (via SocketServer) + emit pamRequest(m_socket, m_auth->request()); + + return; } + + qWarning() << "Display: Unable to handle Auth request!"; } void Display::slotSessionStarted(bool success) { qDebug() << "Session started"; } + + // inform daemon if greeter stopped, so we dont forward + // errors or infos etc. through invalid socket + void Display::slotGreeterStopped() { + qDebug() << "Display: Greeter was stopped"; + m_socket = nullptr; + } } diff --git a/src/daemon/Display.h b/src/daemon/Display.h index 09d3cf9..a05d2ab 100644 --- a/src/daemon/Display.h +++ b/src/daemon/Display.h @@ -60,14 +60,18 @@ namespace SDDM { void login(QLocalSocket *socket, const QString &user, const QString &password, const Session &session); + void setPamResponse(const QString &password); + void cancelPamConv(); bool attemptAutologin(); void displayServerStarted(); signals: void stopped(); - void loginFailed(QLocalSocket *socket); + void loginFailed(QLocalSocket *socket, const QString &message); void loginSucceeded(QLocalSocket *socket); + void pamConvMsg(QLocalSocket *socket, const QString &message); + void pamRequest(QLocalSocket *socket, const AuthRequest * const request); private: QString findGreeterTheme() const; @@ -78,6 +82,7 @@ namespace SDDM { bool m_relogin { true }; bool m_started { false }; + bool m_failed { false }; int m_terminalId { 7 }; @@ -100,6 +105,7 @@ namespace SDDM { void slotHelperFinished(Auth::HelperExitStatus status); void slotAuthInfo(const QString &message, Auth::Info info); void slotAuthError(const QString &message, Auth::Error error); + void slotGreeterStopped(); }; } diff --git a/src/daemon/Greeter.cpp b/src/daemon/Greeter.cpp index 0240085..064703c 100644 --- a/src/daemon/Greeter.cpp +++ b/src/daemon/Greeter.cpp @@ -223,6 +223,8 @@ namespace SDDM { if (!m_process->waitForFinished(5000)) m_process->kill(); } + + emit stopped(); } void Greeter::finished() { @@ -239,6 +241,8 @@ namespace SDDM { // clean up m_process->deleteLater(); m_process = nullptr; + + emit stopped(); } void Greeter::onRequestChanged() { @@ -266,6 +270,8 @@ namespace SDDM { // clean up m_auth->deleteLater(); m_auth = nullptr; + + emit stopped(); } void Greeter::onReadyReadStandardError() diff --git a/src/daemon/Greeter.h b/src/daemon/Greeter.h index 7391a35..6fcf613 100644 --- a/src/daemon/Greeter.h +++ b/src/daemon/Greeter.h @@ -43,6 +43,9 @@ namespace SDDM { void setSocket(const QString &socket); void setTheme(const QString &theme); + signals: + void stopped(); + public slots: bool start(); void stop(); diff --git a/src/daemon/SocketServer.cpp b/src/daemon/SocketServer.cpp index 40d72cc..4df2612 100644 --- a/src/daemon/SocketServer.cpp +++ b/src/daemon/SocketServer.cpp @@ -26,6 +26,8 @@ #include "SocketWriter.h" #include "Utils.h" +#include "AuthBase.h" + #include namespace SDDM { @@ -99,9 +101,13 @@ namespace SDDM { connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater); } + // from greeter (to backend) + void SocketServer::readyRead() { QLocalSocket *socket = qobject_cast(sender()); + static const char *logPrefix = "SocketServer: Message received from greeter:"; + // check socket if (!socket) return; @@ -116,7 +122,7 @@ namespace SDDM { switch (GreeterMessages(message)) { case GreeterMessages::Connect: { // log message - qDebug() << "Message received from greeter: Connect"; + qDebug() << logPrefix << "Connect"; // send capabilities SocketWriter(socket) << quint32(DaemonMessages::Capabilities) << quint32(daemonApp->powerManager()->capabilities()); @@ -130,7 +136,7 @@ namespace SDDM { break; case GreeterMessages::Login: { // log message - qDebug() << "Message received from greeter: Login"; + qDebug() << logPrefix << "Login"; // read username, pasword etc. QString user, password, filename; @@ -141,58 +147,85 @@ namespace SDDM { emit login(socket, user, password, session); } break; + case GreeterMessages::PamResponse: { + qDebug() << logPrefix << "PamResponse"; + + // new password from greeter dialog + QString newPassword; + input >> newPassword; + + // emit signal + emit sendPamResponse(newPassword); + } + break; + case GreeterMessages::PamCancel: { + qDebug() << logPrefix << "PamCancel"; + + // emit signal + emit cancelPamConv(); + } + break; case GreeterMessages::PowerOff: { // log message - qDebug() << "Message received from greeter: PowerOff"; + qDebug() << logPrefix << "PowerOff"; // power off daemonApp->powerManager()->powerOff(); } break; case GreeterMessages::Reboot: { - // log message - qDebug() << "Message received from greeter: Reboot"; + qDebug() << logPrefix << "Reboot"; // reboot daemonApp->powerManager()->reboot(); } break; case GreeterMessages::Suspend: { - // log message - qDebug() << "Message received from greeter: Suspend"; + qDebug() << logPrefix << "Suspend"; // suspend daemonApp->powerManager()->suspend(); } break; case GreeterMessages::Hibernate: { - // log message - qDebug() << "Message received from greeter: Hibernate"; + qDebug() << logPrefix << "Hibernate"; // hibernate daemonApp->powerManager()->hibernate(); } break; case GreeterMessages::HybridSleep: { - // log message - qDebug() << "Message received from greeter: HybridSleep"; + qDebug() << logPrefix << "HybridSleep"; // hybrid sleep daemonApp->powerManager()->hybridSleep(); } break; default: { - // log message - qWarning() << "Unknown message" << message; + qWarning() << logPrefix << "Unknown message" << message; } } } - void SocketServer::loginFailed(QLocalSocket *socket) { - SocketWriter(socket) << quint32(DaemonMessages::LoginFailed); + // from (pam) backend to greeter + + void SocketServer::loginFailed(QLocalSocket *socket, const QString &message) { + SocketWriter(socket) << quint32(DaemonMessages::LoginFailed) << message; } void SocketServer::loginSucceeded(QLocalSocket *socket) { SocketWriter(socket) << quint32(DaemonMessages::LoginSucceeded); } + + void SocketServer::pamConvMsg(QLocalSocket *socket, const QString &message) { + // send pam converse message to greeter + SocketWriter(socket) << quint32(DaemonMessages::PamConvMsg) << message; + } + + void SocketServer::pamRequest(QLocalSocket *socket, const AuthRequest * const pam_request) { + // convert AuthRequest to simple Request + Request request = pam_request->request(); + // send pam request to greeter + SocketWriter(socket) << quint32(DaemonMessages::PamRequest) << request; + } } diff --git a/src/daemon/SocketServer.h b/src/daemon/SocketServer.h index 0d12acb..d3cd85b 100644 --- a/src/daemon/SocketServer.h +++ b/src/daemon/SocketServer.h @@ -25,6 +25,7 @@ #include #include "Session.h" +#include "AuthRequest.h" class QLocalServer; class QLocalSocket; @@ -45,14 +46,20 @@ namespace SDDM { void newConnection(); void readyRead(); - void loginFailed(QLocalSocket *socket); + // from (pam) backend to greeter + void loginFailed(QLocalSocket *socket, const QString &message); void loginSucceeded(QLocalSocket *socket); + void pamConvMsg(QLocalSocket *socket, const QString &message); + void pamRequest(QLocalSocket *socket, const AuthRequest * const request); signals: + // from greeter to (pam) backend void login(QLocalSocket *socket, const QString &user, const QString &password, const Session &session); void connected(); + void sendPamResponse(const QString &newPassword); + void cancelPamConv(); private: QLocalServer *m_server { nullptr }; diff --git a/src/greeter/CMakeLists.txt b/src/greeter/CMakeLists.txt index 12fc0b5..083dca0 100644 --- a/src/greeter/CMakeLists.txt +++ b/src/greeter/CMakeLists.txt @@ -1,6 +1,7 @@ include_directories( "${CMAKE_SOURCE_DIR}/src/common" "${CMAKE_BINARY_DIR}/src/common" + "${CMAKE_SOURCE_DIR}/src/auth" "${LIBXCB_INCLUDE_DIR}" ) @@ -11,6 +12,8 @@ set(GREETER_SOURCES ${CMAKE_SOURCE_DIR}/src/common/SocketWriter.cpp ${CMAKE_SOURCE_DIR}/src/common/ThemeConfig.cpp ${CMAKE_SOURCE_DIR}/src/common/ThemeMetadata.cpp + ${CMAKE_SOURCE_DIR}/src/auth/AuthPrompt.cpp + ${CMAKE_SOURCE_DIR}/src/auth/AuthRequest.cpp GreeterApp.cpp GreeterProxy.cpp KeyboardLayout.cpp diff --git a/src/greeter/GreeterApp.cpp b/src/greeter/GreeterApp.cpp index 5fb70ea..91bb057 100644 --- a/src/greeter/GreeterApp.cpp +++ b/src/greeter/GreeterApp.cpp @@ -28,6 +28,8 @@ #include "ThemeMetadata.h" #include "UserModel.h" #include "KeyboardModel.h" +#include "AuthRequest.h" +#include "AuthPrompt.h" #include "MessageHandler.h" @@ -180,10 +182,15 @@ namespace SDDM { view->rootContext()->setContextProperty(QStringLiteral("userModel"), m_userModel); view->rootContext()->setContextProperty(QStringLiteral("config"), *m_themeConfig); view->rootContext()->setContextProperty(QStringLiteral("sddm"), m_proxy); + view->rootContext()->setContextProperty(QStringLiteral("request"), m_proxy->getRequest()); view->rootContext()->setContextProperty(QStringLiteral("keyboard"), m_keyboard); view->rootContext()->setContextProperty(QStringLiteral("primaryScreen"), QGuiApplication::primaryScreen() == screen); view->rootContext()->setContextProperty(QStringLiteral("__sddm_errors"), QString()); + // register AuthRequest and AuthPrompt type for pamRequest signal + qmlRegisterUncreatableType("SddmAuth", 1, 0, "AuthRequest", QStringLiteral("AuthRequest object creation not allowed")); + qmlRegisterUncreatableType("SddmAuth", 1, 0, "AuthPrompt", QStringLiteral("AuthPrompt object creation not allowed")); + // get theme main script QString mainScript = QStringLiteral("%1/%2").arg(m_themePath).arg(m_metadata->mainScript()); QUrl mainScriptUrl; diff --git a/src/greeter/GreeterProxy.cpp b/src/greeter/GreeterProxy.cpp index 8759e74..e6991a2 100644 --- a/src/greeter/GreeterProxy.cpp +++ b/src/greeter/GreeterProxy.cpp @@ -24,6 +24,8 @@ #include "Messages.h" #include "SessionModel.h" #include "SocketWriter.h" +#include "AuthBase.h" +#include "AuthRequest.h" #include @@ -32,6 +34,7 @@ namespace SDDM { public: SessionModel *sessionModel { nullptr }; QLocalSocket *socket { nullptr }; + AuthRequest *request { nullptr }; QString hostName; bool canPowerOff { false }; bool canReboot { false }; @@ -47,7 +50,8 @@ namespace SDDM { connect(d->socket, &QLocalSocket::disconnected, this, &GreeterProxy::disconnected); connect(d->socket, &QLocalSocket::readyRead, this, &GreeterProxy::readyRead); connect(d->socket, QOverload::of(&QLocalSocket::error), this, &GreeterProxy::error); - + // AuthRequest for exchange of PAM conversation prompts/responses with qml + d->request = new AuthRequest(this); // connect to server d->socket->connectToServer(socket); } @@ -64,6 +68,17 @@ namespace SDDM { d->sessionModel = model; } + /** AuthRequest for exchange of PAM prompts/responses + * between GreeterProxy and greeter GUI + */ + AuthRequest *GreeterProxy::getRequest() { + return d->request; + } + + void GreeterProxy::setRequest(Request *r) { + d->request->setRequest(r); + } + bool GreeterProxy::canPowerOff() const { return d->canPowerOff; } @@ -112,8 +127,6 @@ namespace SDDM { if (!d->sessionModel) { // log error qCritical() << "Session model is not set."; - - // return return; } @@ -127,6 +140,18 @@ namespace SDDM { SocketWriter(d->socket) << quint32(GreeterMessages::Login) << user << password << session; } + // todo: mind session index like for login + void GreeterProxy::pamResponse(const QString &newPassword) { + // send new (renewal) password to daemon for pam conv + SocketWriter(d->socket) << quint32(GreeterMessages::PamResponse) << newPassword; + } + + // todo: mind session index like for login + void GreeterProxy::cancelPamConv() { + // send command to daemon (to pam conv() in backend) + SocketWriter(d->socket) << quint32(GreeterMessages::PamCancel); + } + void GreeterProxy::connected() { // log connection qDebug() << "Connected to the daemon."; @@ -197,11 +222,44 @@ namespace SDDM { } break; case DaemonMessages::LoginFailed: { + QString m; + // read pam conv() message (info/error) from daemon + input >> m; // log message - qDebug() << "Message received from daemon: LoginFailed"; - + qDebug() << "Message received from daemon: LoginFailed, " << m; // emit signal - emit loginFailed(); + emit loginFailed(m); + } + break; + // pam messages from conv() following login, e.g. for expired pwd + // conv() msg with msg_style: PAM_ERROR_MSG or PAM_TEXT_INFO + case DaemonMessages::PamConvMsg: { + QString m; + // read pam conv() message (info/error) from daemon + input >> m; + qDebug() << "PAM conversation message from daemon: " << m; + // send message to GUI + emit pamConvMsg(m); + } + break; + + // new request with prompts from conv() after login, e.g. due to expired pwd + // request contains prompt (message) from conv() with msg_style: + // PAM_PROMPT_ECHO_ON (user name) or PAM_PROMPT_ECHO_OFF (passwords) + case DaemonMessages::PamRequest: { + Request r; + // read request from daemon (Display->SocketServer) + input >> r; + // set pam request for qml and convert Request to AuthRequest + setRequest(&r); + // log prompts + qDebug() << "PAM request with " << r.prompts.length() << " prompts from daemon"; + for (const Prompt &p : r.prompts) + qDebug() << "GreeterProxy: Prompt message=" << p.message << ", hidden=" << p.hidden + << ", type =" << AuthPrompt::typeToString(p.type) << "(" << p.type << ")"; + + // send pam msg's to GUI + emit pamRequest(); } break; default: { diff --git a/src/greeter/GreeterProxy.h b/src/greeter/GreeterProxy.h index ba2533f..76f09c9 100644 --- a/src/greeter/GreeterProxy.h +++ b/src/greeter/GreeterProxy.h @@ -25,8 +25,9 @@ class QLocalSocket; namespace SDDM { + class Request; + class AuthRequest; class SessionModel; - class GreeterProxyPrivate; class GreeterProxy : public QObject { Q_OBJECT @@ -54,6 +55,7 @@ namespace SDDM { bool isConnected() const; void setSessionModel(SessionModel *model); + AuthRequest *getRequest(); public slots: void powerOff(); @@ -62,7 +64,10 @@ namespace SDDM { void hibernate(); void hybridSleep(); + // toward (pam) backend void login(const QString &user, const QString &password, const int sessionIndex) const; + void pamResponse(const QString &newPassword); + void cancelPamConv(); private slots: void connected(); @@ -78,11 +83,15 @@ namespace SDDM { void canHibernateChanged(bool canHibernate); void canHybridSleepChanged(bool canHybridSleep); - void loginFailed(); + // toward qml gui + void loginFailed(const QString err_msg); void loginSucceeded(); + void pamConvMsg(const QString pam_msg); + void pamRequest(); private: GreeterProxyPrivate *d { nullptr }; + void setRequest(Request *r); }; } diff --git a/src/greeter/theme/Main.qml b/src/greeter/theme/Main.qml index 5052688..6543bd5 100644 --- a/src/greeter/theme/Main.qml +++ b/src/greeter/theme/Main.qml @@ -38,15 +38,23 @@ Rectangle { TextConstants { id: textConstants } + function resetTxtMsg() { + txtMessage.text = textConstants.promptSelectUser + txtMessage.color = "white" + } + + // container for password renewal logic + PasswordConnections { + renewalDialog: renewal + pwdItem: listView.currentItem // use listView.currentItem.password + getsBackFocus: listView + errMsg: errMessage + txtMsg: txtMessage + } + Connections { target: sddm - onLoginSucceeded: { - } - - onLoginFailed: { - txtMessage.text = textConstants.loginFailed - listView.currentItem.password = "" - } + onLoginFailed: txtMessage.color = "red" } Background { @@ -117,10 +125,21 @@ Rectangle { color: "#22000000" clip: true + PasswordRenewal { + id: renewal + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: usersContainer.top + visible: false + color: "#22888888" + promptColor: "white" + infosColor: "lightcoral" + } + Item { id: usersContainer width: parent.width; height: 300 anchors.verticalCenter: parent.verticalCenter + enabled: !renewal.visible ImageButton { id: prevUser @@ -149,7 +168,7 @@ Rectangle { delegate: userDelegate orientation: ListView.Horizontal currentIndex: userModel.lastIndex - + onCurrentIndexChanged: resetTxtMsg() KeyNavigation.backtab: prevUser; KeyNavigation.tab: nextUser } @@ -188,7 +207,9 @@ Rectangle { font.pixelSize: 20 visible: __sddm_errors !== "" } + } + } Rectangle { diff --git a/src/helper/HelperApp.cpp b/src/helper/HelperApp.cpp index d942dcc..47a113b 100644 --- a/src/helper/HelperApp.cpp +++ b/src/helper/HelperApp.cpp @@ -164,18 +164,32 @@ namespace SDDM { m_socket->waitForBytesWritten(); } - Request HelperApp::request(const Request& request) { + Request HelperApp::request(const Request& request, bool &cancel) { Msg m = Msg::MSG_UNKNOWN; Request response; SafeDataStream str(m_socket); str << Msg::REQUEST << request; str.send(); + m_socket->waitForBytesWritten(); str.receive(); - str >> m >> response; - if (m != REQUEST) { - response = Request(); - qCritical() << "Received a wrong opcode instead of REQUEST:" << m; - } + str >> m; + switch(m) { + // user response from daemon (greeter) + case REQUEST: + str >> response; + qDebug() << "HelperApp: daemon response received"; + break; + // password renewal canceled in greeter + case CANCEL: + cancel = true; + qDebug() << "HelperApp: Message received from daemon: CANCEL"; + // terminate user session in Auth (QProcess child) + m_session->terminate(); + break; + default: + response = Request(); + qCritical() << "HelperApp: Received a wrong opcode instead of REQUEST or CANCEL:" << m; + } // switch return response; } @@ -185,6 +199,7 @@ namespace SDDM { SafeDataStream str(m_socket); str << Msg::AUTHENTICATED << user; str.send(); + m_socket->waitForBytesWritten(); if (user.isEmpty()) return env; str.receive(); @@ -202,6 +217,7 @@ namespace SDDM { SafeDataStream str(m_socket); str << Msg::SESSION_STATUS << success; str.send(); + m_socket->waitForBytesWritten(); str.receive(); str >> m; if (m != SESSION_STATUS) { diff --git a/src/helper/HelperApp.h b/src/helper/HelperApp.h index b83eade..e07185c 100644 --- a/src/helper/HelperApp.h +++ b/src/helper/HelperApp.h @@ -18,8 +18,8 @@ * */ -#ifndef Auth_H -#define Auth_H +#ifndef HELPERAPP_H +#define HELPERAPP_H #include #include @@ -43,7 +43,7 @@ namespace SDDM { const QString &cookie() const; public slots: - Request request(const Request &request); + Request request(const Request &request, bool &cancel); void info(const QString &message, Auth::Info type); void error(const QString &message, Auth::Error type); QProcessEnvironment authenticated(const QString &user); @@ -52,7 +52,6 @@ namespace SDDM { private slots: void setUp(); void doAuth(); - void sessionFinished(int status); private: @@ -66,4 +65,4 @@ namespace SDDM { }; } -#endif // Auth_H +#endif // HELPERAPP_H diff --git a/src/helper/backend/PamBackend.cpp b/src/helper/backend/PamBackend.cpp index 69cbd2c..4d1a1d0 100644 --- a/src/helper/backend/PamBackend.cpp +++ b/src/helper/backend/PamBackend.cpp @@ -106,11 +106,14 @@ namespace SDDM { /* * Expects an empty prompt list if the previous request has been processed + * + * @return true if new prompt was inserted or prompt message was set + * and false if prompt was found and already sent */ bool PamData::insertPrompt(const struct pam_message* msg, bool predict) { Prompt &p = findPrompt(msg); - // first, check if we already have stored this propmpt + // first, check if we already have stored this prompt if (p.valid()) { // we have a response already - do nothing if (m_sent) @@ -152,13 +155,54 @@ namespace SDDM { return true; } - Auth::Info PamData::handleInfo(const struct pam_message* msg, bool predict) { - if (QString::fromLocal8Bit(msg->msg).indexOf(QRegExp(QStringLiteral("^Changing password for [^ ]+$")))) { - if (predict) + /* + * Distinguish types of pam conversation infos. + * + * Currently different info types are not really used (see \ref Display::slotAuthInfo which + * just fires one signal for all info types). Just builds new request for "changing" keyword. + */ + Auth::Info PamData::handleInfo(const struct pam_message* msg, bool &newRequest, bool predict) { + if (QString::fromLocal8Bit(msg->msg).indexOf(QRegExp(QStringLiteral("^Changing password for [^ ]+$"))) >= 0) + { + if (predict) { m_currentRequest = Request(changePassRequest); + newRequest = true; + m_sent = false; + } return Auth::INFO_PASS_CHANGE_REQUIRED; } - return Auth::INFO_UNKNOWN; + return Auth::INFO_PAM_CONV; + } + + /* Figure out if pam (chAuthTok) sent an error or just an info during conversation + * + * Real error message e.g. "Password change aborted." But most are just info messages + * e.g. "change your password ... (administrator enforced)" with type PAM_ERROR_MSG. + */ + Auth::Error PamData::handleErr(const struct pam_message* msg, bool &newRequest, bool predict) { + // bad password during password renewal (for expired password) + if (QString::fromLocal8Bit(msg->msg).startsWith(QStringLiteral("BAD PASSWORD:"))) + { + if (predict) + { + // prepare new (empty) request, new password prompt will follow + m_currentRequest = Request(); + newRequest = false; + m_sent = false; + } + return Auth::ERROR_NONE; + } + + if(QString::fromLocal8Bit(msg->msg).startsWith(QStringLiteral("Password change aborted."))) { + return Auth::ERROR_AUTHENTICATION; + } + + // first login for user, home dir will be created, ignore error message (which would shutdown sddm) + if(QString::fromLocal8Bit(msg->msg).startsWith(QStringLiteral("Unable to create and initialize directory"))) { + return Auth::ERROR_NONE; + } + + return Auth::ERROR_NONE; } /* @@ -172,6 +216,7 @@ namespace SDDM { return response; } + /* As long as m_currentRequest is not sent (m_sent is false) return current request */ const Request& PamData::getRequest() const { if (!m_sent) return m_currentRequest; @@ -179,6 +224,7 @@ namespace SDDM { return invalidRequest; } + /* Use new request if prompts are equal to current one and set sent true. */ void PamData::completeRequest(const Request& request) { if (request.prompts.length() != m_currentRequest.prompts.length()) { qWarning() << "[PAM] Different request/response list length, ignoring"; @@ -232,6 +278,7 @@ namespace SDDM { } bool PamBackend::authenticate() { + m_convCanceled = false; // reset for converse() if (!m_pam->authenticate()) { m_app->error(m_pam->errorString(), Auth::ERROR_AUTHENTICATION); return false; @@ -291,6 +338,32 @@ namespace SDDM { return QString::fromLocal8Bit((const char*) m_pam->getItem(PAM_USER)); } + /** @internal for debug log: get string representation of pam message msg_style */ + const QString &PamBackend::msgStyleString(int msg_style) + { + static const QString msgStyle[] = { + QStringLiteral("PAM_PROMPT_ECHO_OFF"), + QStringLiteral("PAM_PROMPT_ECHO_ON"), + QStringLiteral("PAM_ERROR_MSG"), + QStringLiteral("PAM_TEXT_INFO"), + QStringLiteral("UNKNOWN"), + }; + + switch(msg_style) { + case PAM_PROMPT_ECHO_OFF: + return msgStyle[0]; break; + case PAM_PROMPT_ECHO_ON: + return msgStyle[1]; break; + case PAM_ERROR_MSG: + return msgStyle[2]; break; + case PAM_TEXT_INFO: + return msgStyle[3]; break; + default: break; + } + + return msgStyle[4]; + } + int PamBackend::converse(int n, const struct pam_message **msg, struct pam_response **resp) { qDebug() << "[PAM] Conversation with" << n << "messages"; @@ -299,34 +372,69 @@ namespace SDDM { if (n <= 0 || n > PAM_MAX_NUM_MSG) return PAM_CONV_ERR; + // see whats going on in pam_conv for (int i = 0; i < n; i++) { + qDebug() << "[PAM] pam_conv: style=" << msgStyleString(msg[i]->msg_style) + << "(" << msg[i]->msg_style << "), msg[" << i << "]=" << QString::fromLocal8Bit(msg[i]->msg); + } + + for (int i = 0; i < n; i++) { + + QString convMsg = QString::fromLocal8Bit(msg[i]->msg); + Auth::Error convErr; + switch(msg[i]->msg_style) { case PAM_PROMPT_ECHO_OFF: case PAM_PROMPT_ECHO_ON: newRequest = m_data->insertPrompt(msg[i], n == 1); break; case PAM_ERROR_MSG: - m_app->error(QString::fromLocal8Bit(msg[i]->msg), Auth::ERROR_AUTHENTICATION); + convErr = m_data->handleErr(msg[i], newRequest, n == 1); + // upon pwd change pam sends error message and then starts conversation, + // so treat most of them actually as infos + if(convErr==Auth::ERROR_NONE) + m_app->info(convMsg, Auth::INFO_PAM_CONV); + else { + qDebug() << "[PAM] PamBackend: pam error message, msg=" << convMsg; + m_app->error(convMsg, Auth::ERROR_AUTHENTICATION /* convErr */); + return PAM_CONV_ERR; + } break; case PAM_TEXT_INFO: + // send pam conversation msg to greeter via HelperApp, SocketServer, Display // if there's only the info message, let's predict the prompts too - m_app->info(QString::fromLocal8Bit(msg[i]->msg), m_data->handleInfo(msg[i], n == 1)); + m_app->info(convMsg, m_data->handleInfo(msg[i], newRequest, n == 1)); break; default: break; } } + // pam (chauthtok) does not shut up after canceled with PAM_CONV_ERR in previous pam_conv + if(m_convCanceled) { + qDebug() << "[PAM] PamBackend: conversation canceled, dump trailing messages"; + return PAM_CONV_ERR; + } + if (newRequest) { - Request sent = m_data->getRequest(); + // get current request + Request send = m_data->getRequest(); Request received; - if (sent.valid()) { - received = m_app->request(sent); + // any prompt? + if (send.valid()) { + // send request to daemon (ask for a response) + received = m_app->request(send, m_convCanceled); + + // password input canceled in greeter + if (m_convCanceled) { + return PAM_CONV_ERR; + } if (!received.valid()) return PAM_CONV_ERR; + // compare request with response m_data->completeRequest(received); } } diff --git a/src/helper/backend/PamBackend.h b/src/helper/backend/PamBackend.h index 4c8b4b3..8784f21 100644 --- a/src/helper/backend/PamBackend.h +++ b/src/helper/backend/PamBackend.h @@ -37,7 +37,8 @@ namespace SDDM { PamData(); bool insertPrompt(const struct pam_message *msg, bool predict = true); - Auth::Info handleInfo(const struct pam_message *msg, bool predict); + Auth::Info handleInfo(const struct pam_message *msg, bool &newRequest, bool predict); + Auth::Error handleErr(const struct pam_message *msg, bool &newRequest, bool predict); const Request& getRequest() const; void completeRequest(const Request& request); @@ -71,8 +72,10 @@ namespace SDDM { virtual QString userName(); private: + bool m_convCanceled { false }; PamData *m_data { nullptr }; PamHandle *m_pam { nullptr }; + static const QString &msgStyleString(int msg_style); }; } diff --git a/src/helper/backend/PasswdBackend.cpp b/src/helper/backend/PasswdBackend.cpp index 8a0b8c9..27e7435 100644 --- a/src/helper/backend/PasswdBackend.cpp +++ b/src/helper/backend/PasswdBackend.cpp @@ -55,7 +55,9 @@ namespace SDDM { r.prompts << Prompt(AuthPrompt::LOGIN_USER, QStringLiteral("Login"), false); r.prompts << Prompt(AuthPrompt::LOGIN_PASSWORD, QStringLiteral("Password"), true); - Request response = m_app->request(r); + bool canceled = false; + Request response = m_app->request(r,canceled); + if(canceled==true) return false; // canceled in greeter for(const Prompt &p : qAsConst(response.prompts)) { switch (p.type) { case AuthPrompt::LOGIN_USER: