service/pam: move pam execution to subprocess to allow killing it
Many pam modules can't be aborted well without this.
This commit is contained in:
parent
b5c8774a79
commit
e89035b18c
|
@ -28,6 +28,7 @@ Checks: >
|
||||||
-modernize-return-braced-init-list,
|
-modernize-return-braced-init-list,
|
||||||
-modernize-use-trailing-return-type,
|
-modernize-use-trailing-return-type,
|
||||||
performance-*,
|
performance-*,
|
||||||
|
-performance-avoid-endl,
|
||||||
portability-std-allocator-const,
|
portability-std-allocator-const,
|
||||||
readability-*,
|
readability-*,
|
||||||
-readability-function-cognitive-complexity,
|
-readability-function-cognitive-complexity,
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
qt_add_library(quickshell-service-pam STATIC
|
qt_add_library(quickshell-service-pam STATIC
|
||||||
qml.cpp
|
qml.cpp
|
||||||
conversation.cpp
|
conversation.cpp
|
||||||
|
ipc.cpp
|
||||||
|
subprocess.cpp
|
||||||
)
|
)
|
||||||
qt_add_qml_module(quickshell-service-pam
|
qt_add_qml_module(quickshell-service-pam
|
||||||
URI Quickshell.Services.Pam
|
URI Quickshell.Services.Pam
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
#include "conversation.hpp"
|
#include "conversation.hpp"
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qmutex.h>
|
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
|
#include <qsocketnotifier.h>
|
||||||
#include <qstring.h>
|
#include <qstring.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <security/_pam_types.h>
|
#include <sys/wait.h>
|
||||||
#include <security/pam_appl.h>
|
|
||||||
|
#include "ipc.hpp"
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg);
|
Q_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg);
|
||||||
|
|
||||||
QString PamError::toString(PamError::Enum value) {
|
QString PamError::toString(PamError::Enum value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case ConnectionFailed: return "Failed to connect to pam";
|
case StartFailed: return "Failed to start the PAM session";
|
||||||
case TryAuthFailed: return "Failed to try authenticating";
|
case TryAuthFailed: return "Failed to try authenticating";
|
||||||
|
case InternalError: return "Internal error occurred";
|
||||||
default: return "Invalid error";
|
default: return "Invalid error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,160 +27,116 @@ QString PamResult::toString(PamResult::Enum value) {
|
||||||
case Failed: return "Failed";
|
case Failed: return "Failed";
|
||||||
case Error: return "Error occurred while authenticating";
|
case Error: return "Error occurred while authenticating";
|
||||||
case MaxTries: return "The authentication method has no more attempts available";
|
case MaxTries: return "The authentication method has no more attempts available";
|
||||||
// case Expired: return "The account has expired";
|
|
||||||
// case PermissionDenied: return "Permission denied";
|
|
||||||
default: return "Invalid result";
|
default: return "Invalid result";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PamConversation::run() {
|
PamConversation::~PamConversation() { this->abort(); }
|
||||||
auto conv = pam_conv {
|
|
||||||
.conv = &PamConversation::conversation,
|
|
||||||
.appdata_ptr = this,
|
|
||||||
};
|
|
||||||
|
|
||||||
pam_handle_t* handle = nullptr;
|
void PamConversation::start(const QString& configDir, const QString& config, const QString& user) {
|
||||||
|
this->childPid = PamConversation::createSubprocess(&this->pipes, configDir, config, user);
|
||||||
qCInfo(logPam) << this << "Starting pam session for user" << this->user << "with config"
|
if (this->childPid == 0) {
|
||||||
<< this->config << "in configdir" << this->configDir;
|
qCCritical(logPam) << "Failed to create pam subprocess.";
|
||||||
|
emit this->error(PamError::InternalError);
|
||||||
auto result = pam_start_confdir(
|
|
||||||
this->config.toStdString().c_str(),
|
|
||||||
this->user.toStdString().c_str(),
|
|
||||||
&conv,
|
|
||||||
this->configDir.toStdString().c_str(),
|
|
||||||
&handle
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != PAM_SUCCESS) {
|
|
||||||
qCCritical(logPam) << this << "Unable to start pam conversation with error"
|
|
||||||
<< QString(pam_strerror(handle, result));
|
|
||||||
emit this->error(PamError::ConnectionFailed);
|
|
||||||
this->deleteLater();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = pam_authenticate(handle, 0);
|
QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PamConversation::onMessage);
|
||||||
|
this->notifier.setSocket(this->pipes.fdIn);
|
||||||
// Seems to require root and quickshell should not run as root.
|
this->notifier.setEnabled(true);
|
||||||
// if (result == PAM_SUCCESS) {
|
|
||||||
// result = pam_acct_mgmt(handle, 0);
|
|
||||||
// }
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case PAM_SUCCESS:
|
|
||||||
qCInfo(logPam) << this << "ended with successful authentication.";
|
|
||||||
emit this->completed(PamResult::Success);
|
|
||||||
break;
|
|
||||||
case PAM_AUTH_ERR:
|
|
||||||
qCInfo(logPam) << this << "ended with failed authentication.";
|
|
||||||
emit this->completed(PamResult::Failed);
|
|
||||||
break;
|
|
||||||
case PAM_MAXTRIES:
|
|
||||||
qCInfo(logPam) << this << "ended with failure: max tries.";
|
|
||||||
emit this->completed(PamResult::MaxTries);
|
|
||||||
break;
|
|
||||||
/*case PAM_ACCT_EXPIRED:
|
|
||||||
qCInfo(logPam) << this << "ended with failure: account expiration.";
|
|
||||||
emit this->completed(PamResult::Expired);
|
|
||||||
break;
|
|
||||||
case PAM_PERM_DENIED:
|
|
||||||
qCInfo(logPam) << this << "ended with failure: permission denied.";
|
|
||||||
emit this->completed(PamResult::PermissionDenied);
|
|
||||||
break;*/
|
|
||||||
default:
|
|
||||||
qCCritical(logPam) << this << "ended with error:" << QString(pam_strerror(handle, result));
|
|
||||||
emit this->error(PamError::TryAuthFailed);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = pam_end(handle, result);
|
|
||||||
if (result != PAM_SUCCESS) {
|
|
||||||
qCCritical(logPam) << this << "Failed to end pam conversation with error code"
|
|
||||||
<< QString(pam_strerror(handle, result));
|
|
||||||
}
|
|
||||||
|
|
||||||
this->deleteLater();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PamConversation::abort() {
|
void PamConversation::abort() {
|
||||||
qCDebug(logPam) << "Abort requested for" << this;
|
if (this->childPid != 0) {
|
||||||
auto locker = QMutexLocker(&this->wakeMutex);
|
qCDebug(logPam) << "Killing subprocess for" << this;
|
||||||
this->mAbort = true;
|
kill(this->childPid, SIGKILL); // NOLINT (include)
|
||||||
this->waker.wakeOne();
|
waitpid(this->childPid, nullptr, 0);
|
||||||
|
this->childPid = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PamConversation::respond(QString response) {
|
void PamConversation::internalError() {
|
||||||
qCDebug(logPam) << "Set response for" << this;
|
if (this->childPid != 0) {
|
||||||
auto locker = QMutexLocker(&this->wakeMutex);
|
qCDebug(logPam) << "Killing subprocess for" << this;
|
||||||
this->response = std::move(response);
|
kill(this->childPid, SIGKILL); // NOLINT (include)
|
||||||
this->hasResponse = true;
|
waitpid(this->childPid, nullptr, 0);
|
||||||
this->waker.wakeOne();
|
this->childPid = 0;
|
||||||
|
emit this->error(PamError::InternalError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int PamConversation::conversation(
|
void PamConversation::respond(const QString& response) {
|
||||||
int msgCount,
|
qCDebug(logPam) << "Sending response for" << this;
|
||||||
const pam_message** msgArray,
|
if (!this->pipes.writeString(response.toStdString())) {
|
||||||
pam_response** responseArray,
|
qCCritical(logPam) << "Failed to write response to subprocess.";
|
||||||
void* appdata
|
this->internalError();
|
||||||
) {
|
}
|
||||||
auto* delegate = static_cast<PamConversation*>(appdata);
|
}
|
||||||
|
|
||||||
|
void PamConversation::onMessage() {
|
||||||
{
|
{
|
||||||
auto locker = QMutexLocker(&delegate->wakeMutex);
|
qCDebug(logPam) << "Got message from subprocess.";
|
||||||
if (delegate->mAbort) {
|
|
||||||
return PAM_ERROR_MSG;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// freed by libc so must be alloc'd by it.
|
auto type = PamIpcEvent::Exit;
|
||||||
auto* responses = static_cast<pam_response*>(calloc(msgCount, sizeof(pam_response))); // NOLINT
|
|
||||||
|
|
||||||
for (auto i = 0; i < msgCount; i++) {
|
auto ok = this->pipes.readBytes(
|
||||||
const auto* message = msgArray[i]; // NOLINT
|
reinterpret_cast<char*>(&type), // NOLINT
|
||||||
auto& response = responses[i]; // NOLINT
|
sizeof(PamIpcEvent)
|
||||||
|
);
|
||||||
|
|
||||||
auto msgString = QString(message->msg);
|
if (!ok) goto fail;
|
||||||
auto messageChanged = true; // message->msg_style != PAM_PROMPT_ECHO_OFF;
|
|
||||||
auto isError = message->msg_style == PAM_ERROR_MSG;
|
|
||||||
auto responseRequired =
|
|
||||||
message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON;
|
|
||||||
|
|
||||||
qCDebug(logPam) << delegate << "got new message message:" << msgString
|
if (type == PamIpcEvent::Exit) {
|
||||||
<< "messageChanged:" << messageChanged << "isError:" << isError
|
auto code = PamIpcExitCode::OtherError;
|
||||||
<< "responseRequired" << responseRequired;
|
|
||||||
|
|
||||||
delegate->hasResponse = false;
|
ok = this->pipes.readBytes(
|
||||||
emit delegate->message(msgString, messageChanged, isError, responseRequired);
|
reinterpret_cast<char*>(&code), // NOLINT
|
||||||
|
sizeof(PamIpcExitCode)
|
||||||
|
);
|
||||||
|
|
||||||
{
|
if (!ok) goto fail;
|
||||||
auto locker = QMutexLocker(&delegate->wakeMutex);
|
|
||||||
|
|
||||||
if (delegate->mAbort) {
|
qCDebug(logPam) << "Subprocess exited with code" << static_cast<int>(code);
|
||||||
free(responses); // NOLINT
|
|
||||||
return PAM_ERROR_MSG;
|
switch (code) {
|
||||||
|
case PamIpcExitCode::Success: emit this->completed(PamResult::Success); break;
|
||||||
|
case PamIpcExitCode::AuthFailed: emit this->completed(PamResult::Failed); break;
|
||||||
|
case PamIpcExitCode::StartFailed: emit this->error(PamError::StartFailed); break;
|
||||||
|
case PamIpcExitCode::MaxTries: emit this->completed(PamResult::MaxTries); break;
|
||||||
|
case PamIpcExitCode::PamError: emit this->error(PamError::TryAuthFailed); break;
|
||||||
|
case PamIpcExitCode::OtherError: emit this->error(PamError::InternalError); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseRequired) {
|
waitpid(this->childPid, nullptr, 0);
|
||||||
if (!delegate->hasResponse) {
|
this->childPid = 0;
|
||||||
delegate->waker.wait(locker.mutex());
|
} else if (type == PamIpcEvent::Request) {
|
||||||
|
PamIpcRequestFlags flags {};
|
||||||
|
|
||||||
if (delegate->mAbort) {
|
ok = this->pipes.readBytes(
|
||||||
free(responses); // NOLINT
|
reinterpret_cast<char*>(&flags), // NOLINT
|
||||||
return PAM_ERROR_MSG;
|
sizeof(PamIpcRequestFlags)
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (!delegate->hasResponse) {
|
if (!ok) goto fail;
|
||||||
qCCritical(logPam
|
|
||||||
) << "Pam conversation requires response and does not have one. This should not happen.";
|
|
||||||
}
|
|
||||||
|
|
||||||
response.resp = strdup(delegate->response.toStdString().c_str()); // NOLINT (include error)
|
auto message = this->pipes.readString(&ok);
|
||||||
}
|
|
||||||
|
if (!ok) goto fail;
|
||||||
|
|
||||||
|
this->message(
|
||||||
|
QString::fromUtf8(message),
|
||||||
|
/*flags.echo*/ true,
|
||||||
|
flags.error,
|
||||||
|
flags.responseRequired
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
qCCritical(logPam) << "Unexpected message from subprocess.";
|
||||||
|
goto fail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
*responseArray = responses;
|
fail:
|
||||||
return PAM_SUCCESS;
|
qCCritical(logPam) << "Failed to read subprocess request.";
|
||||||
|
this->internalError();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,16 @@
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <qmutex.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
#include <qthread.h>
|
#include <qsocketnotifier.h>
|
||||||
|
#include <qtclasshelpermacros.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qwaitcondition.h>
|
|
||||||
#include <security/pam_appl.h>
|
#include "ipc.hpp"
|
||||||
|
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(logPam);
|
||||||
|
|
||||||
/// The result of an authentication.
|
/// The result of an authentication.
|
||||||
class PamResult: public QObject {
|
class PamResult: public QObject {
|
||||||
|
@ -26,10 +29,6 @@ public:
|
||||||
Error = 2,
|
Error = 2,
|
||||||
/// The authentication method ran out of tries and should not be used again.
|
/// The authentication method ran out of tries and should not be used again.
|
||||||
MaxTries = 3,
|
MaxTries = 3,
|
||||||
// The account has expired.
|
|
||||||
// Expired 4,
|
|
||||||
// Permission denied.
|
|
||||||
// PermissionDenied 5,
|
|
||||||
};
|
};
|
||||||
Q_ENUM(Enum);
|
Q_ENUM(Enum);
|
||||||
|
|
||||||
|
@ -44,52 +43,56 @@ class PamError: public QObject {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Enum {
|
enum Enum {
|
||||||
/// Failed to initiate the pam connection.
|
/// Failed to start the pam session.
|
||||||
ConnectionFailed = 1,
|
StartFailed = 1,
|
||||||
/// Failed to try to authenticate the user.
|
/// Failed to try to authenticate the user.
|
||||||
/// This is not the same as the user failing to authenticate.
|
/// This is not the same as the user failing to authenticate.
|
||||||
TryAuthFailed = 2,
|
TryAuthFailed = 2,
|
||||||
|
/// An error occurred inside quickshell's pam interface.
|
||||||
|
InternalError = 3,
|
||||||
};
|
};
|
||||||
Q_ENUM(Enum);
|
Q_ENUM(Enum);
|
||||||
|
|
||||||
Q_INVOKABLE static QString toString(PamError::Enum value);
|
Q_INVOKABLE static QString toString(PamError::Enum value);
|
||||||
};
|
};
|
||||||
|
|
||||||
class PamConversation: public QThread {
|
// PAM has no way to abort a running module except when it sends a message,
|
||||||
|
// meaning aborts for things like fingerprint scanners
|
||||||
|
// and hardware keys don't actually work without aborting the process...
|
||||||
|
// so we have a subprocess.
|
||||||
|
class PamConversation: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit PamConversation(QString config, QString configDir, QString user)
|
explicit PamConversation(QObject* parent): QObject(parent) {}
|
||||||
: config(std::move(config))
|
~PamConversation() override;
|
||||||
, configDir(std::move(configDir))
|
Q_DISABLE_COPY_MOVE(PamConversation);
|
||||||
, user(std::move(user)) {}
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void run() override;
|
void start(const QString& configDir, const QString& config, const QString& user);
|
||||||
|
|
||||||
void abort();
|
void abort();
|
||||||
void respond(QString response);
|
void respond(const QString& response);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void completed(PamResult::Enum result);
|
void completed(PamResult::Enum result);
|
||||||
void error(PamError::Enum error);
|
void error(PamError::Enum error);
|
||||||
void message(QString message, bool messageChanged, bool isError, bool responseRequired);
|
void message(QString message, bool messageChanged, bool isError, bool responseRequired);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onMessage();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static int conversation(
|
static pid_t createSubprocess(
|
||||||
int msgCount,
|
PamIpcPipes* pipes,
|
||||||
const pam_message** msgArray,
|
const QString& configDir,
|
||||||
pam_response** responseArray,
|
const QString& config,
|
||||||
void* appdata
|
const QString& user
|
||||||
);
|
);
|
||||||
|
|
||||||
QString config;
|
void internalError();
|
||||||
QString configDir;
|
|
||||||
QString user;
|
|
||||||
|
|
||||||
QMutex wakeMutex;
|
pid_t childPid = 0;
|
||||||
QWaitCondition waker;
|
PamIpcPipes pipes;
|
||||||
bool mAbort = false;
|
QSocketNotifier notifier {QSocketNotifier::Read};
|
||||||
bool hasResponse = false;
|
|
||||||
QString response;
|
|
||||||
};
|
};
|
||||||
|
|
69
src/services/pam/ipc.cpp
Normal file
69
src/services/pam/ipc.cpp
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
#include "ipc.hpp"
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
PamIpcPipes::~PamIpcPipes() {
|
||||||
|
if (this->fdIn != 0) close(this->fdIn);
|
||||||
|
if (this->fdOut != 0) close(this->fdOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PamIpcPipes::readBytes(char* buffer, size_t length) const {
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
while (i < length) {
|
||||||
|
auto count = read(this->fdIn, buffer + i, length - i); // NOLINT
|
||||||
|
|
||||||
|
if (count == -1 || count == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PamIpcPipes::writeBytes(const char* buffer, size_t length) const {
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < length) {
|
||||||
|
auto count = write(this->fdOut, buffer + i, length - i); // NOLINT
|
||||||
|
|
||||||
|
if (count == -1 || count == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PamIpcPipes::readString(bool* ok) const {
|
||||||
|
if (ok != nullptr) *ok = false;
|
||||||
|
|
||||||
|
uint32_t length = 0;
|
||||||
|
if (!this->readBytes(reinterpret_cast<char*>(&length), sizeof(length))) { // NOLINT
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
char data[length]; // NOLINT
|
||||||
|
if (!this->readBytes(data, length)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok != nullptr) *ok = true;
|
||||||
|
|
||||||
|
return std::string(data, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PamIpcPipes::writeString(const std::string& string) const {
|
||||||
|
uint32_t length = string.length();
|
||||||
|
if (!this->writeBytes(reinterpret_cast<char*>(&length), sizeof(length))) { // NOLINT
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this->writeBytes(string.data(), string.length());
|
||||||
|
}
|
43
src/services/pam/ipc.hpp
Normal file
43
src/services/pam/ipc.hpp
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <qtclasshelpermacros.h>
|
||||||
|
|
||||||
|
enum class PamIpcEvent : uint8_t {
|
||||||
|
Request,
|
||||||
|
Exit,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class PamIpcExitCode : uint8_t {
|
||||||
|
Success,
|
||||||
|
StartFailed,
|
||||||
|
AuthFailed,
|
||||||
|
MaxTries,
|
||||||
|
PamError,
|
||||||
|
OtherError,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PamIpcRequestFlags {
|
||||||
|
bool echo;
|
||||||
|
bool error;
|
||||||
|
bool responseRequired;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PamIpcPipes {
|
||||||
|
public:
|
||||||
|
explicit PamIpcPipes() = default;
|
||||||
|
explicit PamIpcPipes(int fdIn, int fdOut): fdIn(fdIn), fdOut(fdOut) {}
|
||||||
|
~PamIpcPipes();
|
||||||
|
Q_DISABLE_COPY_MOVE(PamIpcPipes);
|
||||||
|
|
||||||
|
[[nodiscard]] bool readBytes(char* buffer, size_t length) const;
|
||||||
|
[[nodiscard]] bool writeBytes(const char* buffer, size_t length) const;
|
||||||
|
[[nodiscard]] std::string readString(bool* ok) const;
|
||||||
|
[[nodiscard]] bool writeString(const std::string& string) const;
|
||||||
|
|
||||||
|
int fdIn = 0;
|
||||||
|
int fdOut = 0;
|
||||||
|
};
|
|
@ -13,12 +13,6 @@
|
||||||
|
|
||||||
#include "conversation.hpp"
|
#include "conversation.hpp"
|
||||||
|
|
||||||
PamContext::~PamContext() {
|
|
||||||
if (this->conversation != nullptr && this->conversation->isRunning()) {
|
|
||||||
this->conversation->abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PamContext::componentComplete() {
|
void PamContext::componentComplete() {
|
||||||
this->postInit = true;
|
this->postInit = true;
|
||||||
|
|
||||||
|
@ -91,12 +85,12 @@ void PamContext::startConversation() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this->conversation = new PamConversation(this->mConfig, this->mConfigDirectory, user);
|
this->conversation = new PamConversation(this);
|
||||||
QObject::connect(this->conversation, &PamConversation::completed, this, &PamContext::onCompleted);
|
QObject::connect(this->conversation, &PamConversation::completed, this, &PamContext::onCompleted);
|
||||||
QObject::connect(this->conversation, &PamConversation::error, this, &PamContext::onError);
|
QObject::connect(this->conversation, &PamConversation::error, this, &PamContext::onError);
|
||||||
QObject::connect(this->conversation, &PamConversation::message, this, &PamContext::onMessage);
|
QObject::connect(this->conversation, &PamConversation::message, this, &PamContext::onMessage);
|
||||||
emit this->activeChanged();
|
emit this->activeChanged();
|
||||||
this->conversation->start();
|
this->conversation->start(this->mConfigDirectory, this->mConfig, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PamContext::abortConversation() {
|
void PamContext::abortConversation() {
|
||||||
|
@ -104,7 +98,7 @@ void PamContext::abortConversation() {
|
||||||
this->mTargetActive = false;
|
this->mTargetActive = false;
|
||||||
|
|
||||||
QObject::disconnect(this->conversation, nullptr, this, nullptr);
|
QObject::disconnect(this->conversation, nullptr, this, nullptr);
|
||||||
if (this->conversation->isRunning()) this->conversation->abort();
|
this->conversation->deleteLater();
|
||||||
this->conversation = nullptr;
|
this->conversation = nullptr;
|
||||||
emit this->activeChanged();
|
emit this->activeChanged();
|
||||||
|
|
||||||
|
@ -124,9 +118,9 @@ void PamContext::abortConversation() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PamContext::respond(QString response) {
|
void PamContext::respond(const QString& response) {
|
||||||
if (this->isActive() && this->mIsResponseRequired) {
|
if (this->isActive() && this->mIsResponseRequired) {
|
||||||
this->conversation->respond(std::move(response));
|
this->conversation->respond(response);
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "PamContext response was ignored as this context does not require one.";
|
qWarning() << "PamContext response was ignored as this context does not require one.";
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,8 +51,6 @@ class PamContext
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit PamContext(QObject* parent = nullptr): QObject(parent) {}
|
explicit PamContext(QObject* parent = nullptr): QObject(parent) {}
|
||||||
~PamContext() override;
|
|
||||||
Q_DISABLE_COPY_MOVE(PamContext);
|
|
||||||
|
|
||||||
void classBegin() override {}
|
void classBegin() override {}
|
||||||
void componentComplete() override;
|
void componentComplete() override;
|
||||||
|
@ -69,7 +67,7 @@ public:
|
||||||
/// Respond to pam.
|
/// Respond to pam.
|
||||||
///
|
///
|
||||||
/// May not be called unless `responseRequired` is true.
|
/// May not be called unless `responseRequired` is true.
|
||||||
Q_INVOKABLE void respond(QString response);
|
Q_INVOKABLE void respond(const QString& response);
|
||||||
|
|
||||||
[[nodiscard]] bool isActive() const;
|
[[nodiscard]] bool isActive() const;
|
||||||
void setActive(bool active);
|
void setActive(bool active);
|
||||||
|
|
209
src/services/pam/subprocess.cpp
Normal file
209
src/services/pam/subprocess.cpp
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
#include "subprocess.hpp"
|
||||||
|
#include <array>
|
||||||
|
#include <ostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qloggingcategory.h>
|
||||||
|
#include <qstring.h>
|
||||||
|
#include <sched.h>
|
||||||
|
#include <security/_pam_types.h>
|
||||||
|
#include <security/pam_appl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "conversation.hpp"
|
||||||
|
#include "ipc.hpp"
|
||||||
|
|
||||||
|
pid_t PamConversation::createSubprocess(
|
||||||
|
PamIpcPipes* pipes,
|
||||||
|
const QString& configDir,
|
||||||
|
const QString& config,
|
||||||
|
const QString& user
|
||||||
|
) {
|
||||||
|
auto toSubprocess = std::array<int, 2>();
|
||||||
|
auto fromSubprocess = std::array<int, 2>();
|
||||||
|
|
||||||
|
if (pipe(toSubprocess.data()) == -1 || pipe(fromSubprocess.data()) == -1) {
|
||||||
|
qCDebug(logPam) << "Failed to create pipes for subprocess.";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* configDirF = strdup(configDir.toStdString().c_str()); // NOLINT (include)
|
||||||
|
auto* configF = strdup(config.toStdString().c_str()); // NOLINT (include)
|
||||||
|
auto* userF = strdup(user.toStdString().c_str()); // NOLINT (include)
|
||||||
|
auto log = logPam().isDebugEnabled();
|
||||||
|
|
||||||
|
auto pid = fork();
|
||||||
|
|
||||||
|
if (pid < 0) {
|
||||||
|
qCDebug(logPam) << "Failed to fork for subprocess.";
|
||||||
|
} else if (pid == 0) {
|
||||||
|
close(toSubprocess[1]); // close w
|
||||||
|
close(fromSubprocess[0]); // close r
|
||||||
|
|
||||||
|
{
|
||||||
|
auto subprocess = PamSubprocess(log, toSubprocess[0], fromSubprocess[1]);
|
||||||
|
auto code = subprocess.exec(configDirF, configF, userF);
|
||||||
|
subprocess.sendCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(configDirF); // NOLINT
|
||||||
|
free(configF); // NOLINT
|
||||||
|
free(userF); // NOLINT
|
||||||
|
|
||||||
|
// do not do cleanup that may affect the parent
|
||||||
|
_exit(0);
|
||||||
|
} else {
|
||||||
|
close(toSubprocess[0]); // close r
|
||||||
|
close(fromSubprocess[1]); // close w
|
||||||
|
|
||||||
|
pipes->fdIn = fromSubprocess[0];
|
||||||
|
pipes->fdOut = toSubprocess[1];
|
||||||
|
|
||||||
|
free(configDirF); // NOLINT
|
||||||
|
free(configF); // NOLINT
|
||||||
|
free(userF); // NOLINT
|
||||||
|
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1; // should never happen but lint
|
||||||
|
}
|
||||||
|
|
||||||
|
PamIpcExitCode PamSubprocess::exec(const char* configDir, const char* config, const char* user) {
|
||||||
|
logIf(this->log) << "Waiting for parent confirmation..." << std::endl;
|
||||||
|
|
||||||
|
auto conv = pam_conv {
|
||||||
|
.conv = &PamSubprocess::conversation,
|
||||||
|
.appdata_ptr = this,
|
||||||
|
};
|
||||||
|
|
||||||
|
pam_handle_t* handle = nullptr;
|
||||||
|
|
||||||
|
logIf(this->log) << "Starting pam session for user \"" << user << "\" with config \"" << config
|
||||||
|
<< "\" in dir \"" << configDir << "\"" << std::endl;
|
||||||
|
|
||||||
|
auto result = pam_start_confdir(config, user, &conv, configDir, &handle);
|
||||||
|
|
||||||
|
if (result != PAM_SUCCESS) {
|
||||||
|
logIf(true) << "Unable to start pam conversation with error \"" << pam_strerror(handle, result)
|
||||||
|
<< "\" (code " << result << ")" << std::endl;
|
||||||
|
return PamIpcExitCode::StartFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = pam_authenticate(handle, 0);
|
||||||
|
PamIpcExitCode code = PamIpcExitCode::OtherError;
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case PAM_SUCCESS:
|
||||||
|
logIf(this->log) << "Authenticated successfully." << std::endl;
|
||||||
|
code = PamIpcExitCode::Success;
|
||||||
|
break;
|
||||||
|
case PAM_AUTH_ERR:
|
||||||
|
logIf(this->log) << "Failed to authenticate." << std::endl;
|
||||||
|
code = PamIpcExitCode::AuthFailed;
|
||||||
|
break;
|
||||||
|
case PAM_MAXTRIES:
|
||||||
|
logIf(this->log) << "Failed to authenticate due to hitting max tries." << std::endl;
|
||||||
|
code = PamIpcExitCode::MaxTries;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logIf(true) << "Error while authenticating: \"" << pam_strerror(handle, result) << "\" (code "
|
||||||
|
<< result << ")" << std::endl;
|
||||||
|
code = PamIpcExitCode::PamError;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = pam_end(handle, result);
|
||||||
|
if (result != PAM_SUCCESS) {
|
||||||
|
logIf(true) << "Error in pam_end: \"" << pam_strerror(handle, result) << "\" (code " << result
|
||||||
|
<< ")" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PamSubprocess::sendCode(PamIpcExitCode code) {
|
||||||
|
{
|
||||||
|
auto eventType = PamIpcEvent::Exit;
|
||||||
|
auto ok = this->pipes.writeBytes(
|
||||||
|
reinterpret_cast<char*>(&eventType), // NOLINT
|
||||||
|
sizeof(PamIpcEvent)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ok) goto fail;
|
||||||
|
|
||||||
|
ok = this->pipes.writeBytes(
|
||||||
|
reinterpret_cast<char*>(&code), // NOLINT
|
||||||
|
sizeof(PamIpcExitCode)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ok) goto fail;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fail:
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int PamSubprocess::conversation(
|
||||||
|
int msgCount,
|
||||||
|
const pam_message** msgArray,
|
||||||
|
pam_response** responseArray,
|
||||||
|
void* appdata
|
||||||
|
) {
|
||||||
|
auto* delegate = static_cast<PamSubprocess*>(appdata);
|
||||||
|
|
||||||
|
// freed by libc so must be alloc'd by it.
|
||||||
|
auto* responses = static_cast<pam_response*>(calloc(msgCount, sizeof(pam_response))); // NOLINT
|
||||||
|
|
||||||
|
for (auto i = 0; i < msgCount; i++) {
|
||||||
|
const auto* message = msgArray[i]; // NOLINT
|
||||||
|
auto& response = responses[i]; // NOLINT
|
||||||
|
|
||||||
|
auto msgString = std::string(message->msg);
|
||||||
|
auto req = PamIpcRequestFlags {
|
||||||
|
.echo = message->msg_style != PAM_PROMPT_ECHO_OFF,
|
||||||
|
.error = message->msg_style == PAM_ERROR_MSG,
|
||||||
|
.responseRequired =
|
||||||
|
message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON,
|
||||||
|
};
|
||||||
|
|
||||||
|
logIf(delegate->log) << "Relaying pam message: \"" << msgString << "\" echo: " << req.echo
|
||||||
|
<< " error: " << req.error << " responseRequired: " << req.responseRequired
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
auto eventType = PamIpcEvent::Request;
|
||||||
|
auto ok = delegate->pipes.writeBytes(
|
||||||
|
reinterpret_cast<char*>(&eventType), // NOLINT
|
||||||
|
sizeof(PamIpcEvent)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ok) goto fail;
|
||||||
|
|
||||||
|
ok = delegate->pipes.writeBytes(
|
||||||
|
reinterpret_cast<const char*>(&req), // NOLINT
|
||||||
|
sizeof(PamIpcRequestFlags)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ok) goto fail;
|
||||||
|
if (!delegate->pipes.writeString(msgString)) goto fail;
|
||||||
|
|
||||||
|
if (req.responseRequired) {
|
||||||
|
auto ok = false;
|
||||||
|
auto resp = delegate->pipes.readString(&ok);
|
||||||
|
if (!ok) _exit(static_cast<int>(PamIpcExitCode::OtherError));
|
||||||
|
logIf(delegate->log) << "Got response for request.";
|
||||||
|
|
||||||
|
response.resp = strdup(resp.c_str()); // NOLINT (include)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*responseArray = responses;
|
||||||
|
return PAM_SUCCESS;
|
||||||
|
|
||||||
|
fail:
|
||||||
|
free(responseArray); // NOLINT
|
||||||
|
_exit(1);
|
||||||
|
}
|
31
src/services/pam/subprocess.hpp
Normal file
31
src/services/pam/subprocess.hpp
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include <security/pam_appl.h>
|
||||||
|
|
||||||
|
#include "ipc.hpp"
|
||||||
|
|
||||||
|
// endls are intentional as it makes debugging much easier when the buffer actually flushes.
|
||||||
|
// NOLINTBEGIN
|
||||||
|
#define logIf(log) \
|
||||||
|
if (log) std::cout << "quickshell.service.pam.subprocess: "
|
||||||
|
// NOLINTEND
|
||||||
|
|
||||||
|
class PamSubprocess {
|
||||||
|
public:
|
||||||
|
explicit PamSubprocess(bool log, int fdIn, int fdOut): log(log), pipes(fdIn, fdOut) {}
|
||||||
|
PamIpcExitCode exec(const char* configDir, const char* config, const char* user);
|
||||||
|
void sendCode(PamIpcExitCode code);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int conversation(
|
||||||
|
int msgCount,
|
||||||
|
const pam_message** msgArray,
|
||||||
|
pam_response** responseArray,
|
||||||
|
void* appdata
|
||||||
|
);
|
||||||
|
|
||||||
|
bool log;
|
||||||
|
PamIpcPipes pipes;
|
||||||
|
};
|
Loading…
Reference in a new issue