forked from quickshell/quickshell
service/pam: add pam service
This commit is contained in:
parent
f655875547
commit
7e5d128a91
|
@ -14,6 +14,7 @@ Checks: >
|
|||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-cppcoreguidelines-avoid-goto,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
google-build-using-namespace.
|
||||
google-explicit-constructor,
|
||||
google-global-names-in-headers,
|
||||
|
|
|
@ -23,6 +23,7 @@ option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
|
|||
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
|
||||
option(SERVICE_PIPEWIRE "PipeWire service" ON)
|
||||
option(SERVICE_MPRIS "Mpris service" ON)
|
||||
option(SERVICE_PAM "Pam service" ON)
|
||||
|
||||
message(STATUS "Quickshell configuration")
|
||||
message(STATUS " Jemalloc: ${USE_JEMALLOC}")
|
||||
|
@ -39,6 +40,7 @@ message(STATUS " Services")
|
|||
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
|
||||
message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
|
||||
message(STATUS " Mpris: ${SERVICE_MPRIS}")
|
||||
message(STATUS " Pam: ${SERVICE_PAM}")
|
||||
message(STATUS " Hyprland: ${HYPRLAND}")
|
||||
if (HYPRLAND)
|
||||
message(STATUS " IPC: ${HYPRLAND_IPC}")
|
||||
|
|
|
@ -45,6 +45,7 @@ quickshell.packages.<system>.default.override {
|
|||
withWayland = true;
|
||||
withX11 = true;
|
||||
withPipewire = true;
|
||||
withPam = true;
|
||||
withHyprland = true;
|
||||
}
|
||||
```
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
wayland-protocols,
|
||||
xorg,
|
||||
pipewire,
|
||||
pam,
|
||||
|
||||
gitRev ? (let
|
||||
headExists = builtins.pathExists ./.git/HEAD;
|
||||
|
@ -31,6 +32,7 @@
|
|||
withWayland ? true,
|
||||
withX11 ? true,
|
||||
withPipewire ? true,
|
||||
withPam ? true,
|
||||
withHyprland ? true,
|
||||
}: buildStdenv.mkDerivation {
|
||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||
|
@ -55,6 +57,7 @@
|
|||
++ (lib.optional withQtSvg qt6.qtsvg)
|
||||
++ (lib.optionals withWayland [ qt6.qtwayland wayland ])
|
||||
++ (lib.optional withX11 xorg.libxcb)
|
||||
++ (lib.optional withPam pam)
|
||||
++ (lib.optional withPipewire pipewire);
|
||||
|
||||
QTWAYLANDSCANNER = lib.optionalString withWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
|
||||
|
@ -74,6 +77,7 @@
|
|||
++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF"
|
||||
++ lib.optional (!withWayland) "-DWAYLAND=OFF"
|
||||
++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF"
|
||||
++ lib.optional (!withPam) "-DSERVICE_PAM=OFF"
|
||||
++ lib.optional (!withHyprland) "-DHYPRLAND=OFF";
|
||||
|
||||
buildPhase = "ninjaBuildPhase";
|
||||
|
|
|
@ -9,3 +9,7 @@ endif()
|
|||
if (SERVICE_MPRIS)
|
||||
add_subdirectory(mpris)
|
||||
endif()
|
||||
|
||||
if (SERVICE_PAM)
|
||||
add_subdirectory(pam)
|
||||
endif()
|
||||
|
|
17
src/services/pam/CMakeLists.txt
Normal file
17
src/services/pam/CMakeLists.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
#find_package(PAM REQUIRED)
|
||||
|
||||
qt_add_library(quickshell-service-pam STATIC
|
||||
qml.cpp
|
||||
conversation.cpp
|
||||
)
|
||||
qt_add_qml_module(quickshell-service-pam
|
||||
URI Quickshell.Services.Pam
|
||||
VERSION 0.1
|
||||
)
|
||||
|
||||
target_link_libraries(quickshell-service-pam PRIVATE ${QT_DEPS} pam ${PAM_LIBRARIES})
|
||||
|
||||
qs_pch(quickshell-service-pam)
|
||||
qs_pch(quickshell-service-pamplugin)
|
||||
|
||||
target_link_libraries(quickshell PRIVATE quickshell-service-pamplugin)
|
185
src/services/pam/conversation.cpp
Normal file
185
src/services/pam/conversation.cpp
Normal file
|
@ -0,0 +1,185 @@
|
|||
#include "conversation.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qmutex.h>
|
||||
#include <qobject.h>
|
||||
#include <qstring.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <security/_pam_types.h>
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg);
|
||||
|
||||
QString PamError::toString(PamError::Enum value) {
|
||||
switch (value) {
|
||||
case ConnectionFailed: return "Failed to connect to pam";
|
||||
case TryAuthFailed: return "Failed to try authenticating";
|
||||
default: return "Invalid error";
|
||||
}
|
||||
}
|
||||
|
||||
QString PamResult::toString(PamResult::Enum value) {
|
||||
switch (value) {
|
||||
case Success: return "Success";
|
||||
case Failed: return "Failed";
|
||||
case Error: return "Error occurred while authenticating";
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
void PamConversation::run() {
|
||||
auto conv = pam_conv {
|
||||
.conv = &PamConversation::conversation,
|
||||
.appdata_ptr = this,
|
||||
};
|
||||
|
||||
pam_handle_t* handle = nullptr;
|
||||
|
||||
qCInfo(logPam) << this << "Starting pam session for user" << this->user << "with config"
|
||||
<< this->config << "in configdir" << this->configDir;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
result = pam_authenticate(handle, 0);
|
||||
|
||||
// Seems to require root and quickshell should not run as root.
|
||||
// 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() {
|
||||
qCDebug(logPam) << "Abort requested for" << this;
|
||||
auto locker = QMutexLocker(&this->wakeMutex);
|
||||
this->mAbort = true;
|
||||
this->waker.wakeOne();
|
||||
}
|
||||
|
||||
void PamConversation::respond(QString response) {
|
||||
qCDebug(logPam) << "Set response for" << this;
|
||||
auto locker = QMutexLocker(&this->wakeMutex);
|
||||
this->response = std::move(response);
|
||||
this->hasResponse = true;
|
||||
this->waker.wakeOne();
|
||||
}
|
||||
|
||||
int PamConversation::conversation(
|
||||
int msgCount,
|
||||
const pam_message** msgArray,
|
||||
pam_response** responseArray,
|
||||
void* appdata
|
||||
) {
|
||||
auto* delegate = static_cast<PamConversation*>(appdata);
|
||||
|
||||
{
|
||||
auto locker = QMutexLocker(&delegate->wakeMutex);
|
||||
if (delegate->mAbort) {
|
||||
return PAM_ERROR_MSG;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = QString(message->msg);
|
||||
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
|
||||
<< "messageChanged:" << messageChanged << "isError:" << isError
|
||||
<< "responseRequired" << responseRequired;
|
||||
|
||||
delegate->hasResponse = false;
|
||||
emit delegate->message(msgString, messageChanged, isError, responseRequired);
|
||||
|
||||
{
|
||||
auto locker = QMutexLocker(&delegate->wakeMutex);
|
||||
|
||||
if (delegate->mAbort) {
|
||||
free(responses); // NOLINT
|
||||
return PAM_ERROR_MSG;
|
||||
}
|
||||
|
||||
if (responseRequired) {
|
||||
if (!delegate->hasResponse) {
|
||||
delegate->waker.wait(locker.mutex());
|
||||
|
||||
if (delegate->mAbort) {
|
||||
free(responses); // NOLINT
|
||||
return PAM_ERROR_MSG;
|
||||
}
|
||||
}
|
||||
|
||||
if (!delegate->hasResponse) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*responseArray = responses;
|
||||
return PAM_SUCCESS;
|
||||
}
|
95
src/services/pam/conversation.hpp
Normal file
95
src/services/pam/conversation.hpp
Normal file
|
@ -0,0 +1,95 @@
|
|||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qmutex.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qthread.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qwaitcondition.h>
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
/// The result of an authentication.
|
||||
class PamResult: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum {
|
||||
/// Authentication was successful.
|
||||
Success = 0,
|
||||
/// Authentication failed.
|
||||
Failed = 1,
|
||||
/// An error occurred while trying to authenticate.
|
||||
Error = 2,
|
||||
/// The authentication method ran out of tries and should not be used again.
|
||||
MaxTries = 3,
|
||||
// The account has expired.
|
||||
// Expired 4,
|
||||
// Permission denied.
|
||||
// PermissionDenied 5,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(PamResult::Enum value);
|
||||
};
|
||||
|
||||
/// An error that occurred during an authentication.
|
||||
class PamError: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum {
|
||||
/// Failed to initiate the pam connection.
|
||||
ConnectionFailed = 1,
|
||||
/// Failed to try to authenticate the user.
|
||||
/// This is not the same as the user failing to authenticate.
|
||||
TryAuthFailed = 2,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(PamError::Enum value);
|
||||
};
|
||||
|
||||
class PamConversation: public QThread {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit PamConversation(QString config, QString configDir, QString user)
|
||||
: config(std::move(config))
|
||||
, configDir(std::move(configDir))
|
||||
, user(std::move(user)) {}
|
||||
|
||||
public:
|
||||
void run() override;
|
||||
|
||||
void abort();
|
||||
void respond(QString response);
|
||||
|
||||
signals:
|
||||
void completed(PamResult::Enum result);
|
||||
void error(PamError::Enum error);
|
||||
void message(QString message, bool messageChanged, bool isError, bool responseRequired);
|
||||
|
||||
private:
|
||||
static int conversation(
|
||||
int msgCount,
|
||||
const pam_message** msgArray,
|
||||
pam_response** responseArray,
|
||||
void* appdata
|
||||
);
|
||||
|
||||
QString config;
|
||||
QString configDir;
|
||||
QString user;
|
||||
|
||||
QMutex wakeMutex;
|
||||
QWaitCondition waker;
|
||||
bool mAbort = false;
|
||||
bool hasResponse = false;
|
||||
QString response;
|
||||
};
|
67
src/services/pam/module.md
Normal file
67
src/services/pam/module.md
Normal file
|
@ -0,0 +1,67 @@
|
|||
name = "Quickshell.Services.Pam"
|
||||
description = "Pam authentication"
|
||||
headers = [
|
||||
"qml.hpp",
|
||||
"conversation.hpp",
|
||||
]
|
||||
-----
|
||||
|
||||
## Writing pam configurations
|
||||
|
||||
It is a good idea to write pam configurations specifically for quickshell
|
||||
if you want to do anything other than match the default login flow.
|
||||
|
||||
A good example of this is having a configuration that allows entering a password
|
||||
or fingerprint in any order.
|
||||
|
||||
### Structure of a pam configuration.
|
||||
Pam configuration files are a list of rules, each on a new line in the following form:
|
||||
```
|
||||
<type> <control_flag> <module_path> [options]
|
||||
```
|
||||
|
||||
Each line runs in order.
|
||||
|
||||
PamContext currently only works with the `auth` type, as other types require root
|
||||
access to check.
|
||||
|
||||
#### Control flags
|
||||
The control flags you're likely to use are `required` and `sufficient`.
|
||||
- `required` rules must pass for authentication to succeed.
|
||||
- `sufficient` rules will bypass any remaining rules and return on success.
|
||||
|
||||
Note that you should have at least one required rule or pam will fail with an undocumented error.
|
||||
|
||||
#### Modules
|
||||
Pam works with a set of modules that handle various authentication mechanisms.
|
||||
Some common ones include `pam_unix.so` which handles passwords and `pam_fprintd.so`
|
||||
which handles fingerprints.
|
||||
|
||||
These modules have options but none are required for basic functionality.
|
||||
|
||||
### Examples
|
||||
|
||||
Authenticate with only a password:
|
||||
```
|
||||
auth required pam_unix.so
|
||||
```
|
||||
|
||||
Authenticate with only a fingerprint:
|
||||
```
|
||||
auth required pam_fprintd.so
|
||||
```
|
||||
|
||||
Try to authenticate with a fingerprint first, but if that fails fall back to a password:
|
||||
```
|
||||
auth sufficient pam_fprintd.so
|
||||
auth required pam_unix.so
|
||||
```
|
||||
|
||||
Require both a fingerprint and a password:
|
||||
```
|
||||
auth required pam_fprintd.so
|
||||
auth required pam_unix.so
|
||||
```
|
||||
|
||||
|
||||
See also: [Oracle: PAM configuration file](https://docs.oracle.com/cd/E19683-01/816-4883/pam-32/index.html)
|
238
src/services/pam/qml.cpp
Normal file
238
src/services/pam/qml.cpp
Normal file
|
@ -0,0 +1,238 @@
|
|||
#include "qml.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <pwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "conversation.hpp"
|
||||
|
||||
PamContext::~PamContext() {
|
||||
if (this->conversation != nullptr && this->conversation->isRunning()) {
|
||||
this->conversation->abort();
|
||||
}
|
||||
}
|
||||
|
||||
void PamContext::componentComplete() {
|
||||
this->postInit = true;
|
||||
|
||||
if (this->mTargetActive) {
|
||||
this->startConversation();
|
||||
}
|
||||
}
|
||||
|
||||
void PamContext::startConversation() {
|
||||
if (!this->postInit || this->conversation != nullptr) return;
|
||||
|
||||
QString user;
|
||||
|
||||
{
|
||||
auto configDirInfo = QFileInfo(this->mConfigDirectory);
|
||||
if (!configDirInfo.isDir()) {
|
||||
qCritical() << "Cannot start" << this << "because specified config directory"
|
||||
<< this->mConfigDirectory << "is not a directory.";
|
||||
this->mTargetActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
auto configFilePath = QDir(this->mConfigDirectory).filePath(this->mConfig);
|
||||
auto configFileInfo = QFileInfo(configFilePath);
|
||||
if (!configFileInfo.isFile()) {
|
||||
qCritical() << "Cannot start" << this << "because specified config file" << configFilePath
|
||||
<< "is not a file.";
|
||||
this->mTargetActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
auto pwuidbufSize = sysconf(_SC_GETPW_R_SIZE_MAX);
|
||||
if (pwuidbufSize == -1) pwuidbufSize = 8192;
|
||||
char pwuidbuf[pwuidbufSize]; // NOLINT
|
||||
|
||||
passwd pwuid {};
|
||||
passwd* pwuidResult = nullptr;
|
||||
|
||||
if (this->mUser.isEmpty()) {
|
||||
auto r = getpwuid_r(getuid(), &pwuid, pwuidbuf, pwuidbufSize, &pwuidResult);
|
||||
if (pwuidResult == nullptr) {
|
||||
qCritical() << "Cannot start" << this << "due to error in getpwuid_r: " << r;
|
||||
this->mTargetActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
user = pwuid.pw_name;
|
||||
} else {
|
||||
auto r = getpwnam_r(
|
||||
this->mUser.toStdString().c_str(),
|
||||
&pwuid,
|
||||
pwuidbuf,
|
||||
pwuidbufSize,
|
||||
&pwuidResult
|
||||
);
|
||||
|
||||
if (pwuidResult == nullptr) {
|
||||
if (r == 0) {
|
||||
qCritical() << "Cannot start" << this
|
||||
<< "because specified user was not found: " << this->mUser;
|
||||
} else {
|
||||
qCritical() << "Cannot start" << this << "due to error in getpwnam_r: " << r;
|
||||
}
|
||||
|
||||
this->mTargetActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
user = pwuid.pw_name;
|
||||
}
|
||||
}
|
||||
|
||||
this->conversation = new PamConversation(this->mConfig, this->mConfigDirectory, user);
|
||||
QObject::connect(this->conversation, &PamConversation::completed, this, &PamContext::onCompleted);
|
||||
QObject::connect(this->conversation, &PamConversation::error, this, &PamContext::onError);
|
||||
QObject::connect(this->conversation, &PamConversation::message, this, &PamContext::onMessage);
|
||||
emit this->activeChanged();
|
||||
this->conversation->start();
|
||||
}
|
||||
|
||||
void PamContext::abortConversation() {
|
||||
if (this->conversation == nullptr) return;
|
||||
this->mTargetActive = false;
|
||||
|
||||
QObject::disconnect(this->conversation, nullptr, this, nullptr);
|
||||
if (this->conversation->isRunning()) this->conversation->abort();
|
||||
this->conversation = nullptr;
|
||||
emit this->activeChanged();
|
||||
|
||||
if (!this->mMessage.isEmpty()) {
|
||||
this->mMessage.clear();
|
||||
emit this->messageChanged();
|
||||
}
|
||||
|
||||
if (this->mMessageIsError) {
|
||||
this->mMessageIsError = false;
|
||||
emit this->messageIsErrorChanged();
|
||||
}
|
||||
|
||||
if (this->mIsResponseRequired) {
|
||||
this->mIsResponseRequired = false;
|
||||
emit this->responseRequiredChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void PamContext::respond(QString response) {
|
||||
if (this->isActive() && this->mIsResponseRequired) {
|
||||
this->conversation->respond(std::move(response));
|
||||
} else {
|
||||
qWarning() << "PamContext response was ignored as this context does not require one.";
|
||||
}
|
||||
}
|
||||
|
||||
bool PamContext::start() {
|
||||
this->setActive(true);
|
||||
return this->isActive();
|
||||
}
|
||||
|
||||
void PamContext::abort() { this->setActive(false); }
|
||||
|
||||
bool PamContext::isActive() const { return this->conversation != nullptr; }
|
||||
|
||||
void PamContext::setActive(bool active) {
|
||||
if (active == this->mTargetActive) return;
|
||||
this->mTargetActive = active;
|
||||
|
||||
if (active) this->startConversation();
|
||||
else this->abortConversation();
|
||||
}
|
||||
|
||||
QString PamContext::config() const { return this->mConfig; }
|
||||
|
||||
void PamContext::setConfig(QString config) {
|
||||
if (config == this->mConfig) return;
|
||||
|
||||
if (this->isActive()) {
|
||||
qCritical() << "Cannot set config on PamContext while it is active.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->mConfig = std::move(config);
|
||||
emit this->configChanged();
|
||||
}
|
||||
|
||||
QString PamContext::configDirectory() const { return this->mConfigDirectory; }
|
||||
|
||||
void PamContext::setConfigDirectory(QString configDirectory) {
|
||||
if (configDirectory == this->mConfigDirectory) return;
|
||||
|
||||
if (this->isActive()) {
|
||||
qCritical() << "Cannot set configDirectory on PamContext while it is active.";
|
||||
return;
|
||||
}
|
||||
|
||||
auto* context = QQmlEngine::contextForObject(this);
|
||||
if (context != nullptr) {
|
||||
configDirectory = context->resolvedUrl(configDirectory).path();
|
||||
}
|
||||
|
||||
this->mConfigDirectory = std::move(configDirectory);
|
||||
emit this->configDirectoryChanged();
|
||||
}
|
||||
|
||||
QString PamContext::user() const { return this->mUser; }
|
||||
|
||||
void PamContext::setUser(QString user) {
|
||||
if (user == this->mUser) return;
|
||||
|
||||
if (this->isActive()) {
|
||||
qCritical() << "Cannot set user on PamContext while it is active.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->mUser = std::move(user);
|
||||
emit this->userChanged();
|
||||
}
|
||||
|
||||
QString PamContext::message() const { return this->mMessage; }
|
||||
bool PamContext::messageIsError() const { return this->mMessageIsError; }
|
||||
bool PamContext::isResponseRequired() const { return this->mIsResponseRequired; }
|
||||
|
||||
void PamContext::onCompleted(PamResult::Enum result) {
|
||||
emit this->completed(result);
|
||||
this->abortConversation();
|
||||
}
|
||||
|
||||
void PamContext::onError(PamError::Enum error) {
|
||||
emit this->error(error);
|
||||
emit this->completed(PamResult::Error);
|
||||
this->abortConversation();
|
||||
}
|
||||
|
||||
void PamContext::onMessage(
|
||||
QString message,
|
||||
bool messageChanged,
|
||||
bool isError,
|
||||
bool responseRequired
|
||||
) {
|
||||
if (messageChanged) {
|
||||
if (message != this->mMessage) {
|
||||
this->mMessage = std::move(message);
|
||||
emit this->messageChanged();
|
||||
}
|
||||
|
||||
if (isError != this->mMessageIsError) {
|
||||
this->mMessageIsError = isError;
|
||||
emit this->messageIsErrorChanged();
|
||||
}
|
||||
}
|
||||
|
||||
if (responseRequired != this->mIsResponseRequired) {
|
||||
this->mIsResponseRequired = responseRequired;
|
||||
emit this->responseRequiredChanged();
|
||||
}
|
||||
|
||||
emit this->pamMessage();
|
||||
}
|
126
src/services/pam/qml.hpp
Normal file
126
src/services/pam/qml.hpp
Normal file
|
@ -0,0 +1,126 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmlparserstatus.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qthread.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <security/_pam_types.h>
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
#include "conversation.hpp"
|
||||
|
||||
///! Connection to pam.
|
||||
/// Connection to pam. See [the module documentation](../) for pam configuration advice.
|
||||
class PamContext
|
||||
: public QObject
|
||||
, public QQmlParserStatus {
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// If the pam context is actively performing an authentication.
|
||||
///
|
||||
/// Setting this value behaves exactly the same as calling `start()` and `abort()`.
|
||||
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged);
|
||||
/// The pam configuration to use. Defaults to "login".
|
||||
///
|
||||
/// The configuration should name a file inside `configDirectory`.
|
||||
///
|
||||
/// This property may not be set while `active` is true.
|
||||
Q_PROPERTY(QString config READ config WRITE setConfig NOTIFY configChanged);
|
||||
/// The pam configuration directory to use. Defaults to "/etc/pam.d".
|
||||
///
|
||||
/// The configuration directory is resolved relative to the current file if not an absolute path.
|
||||
///
|
||||
/// This property may not be set while `active` is true.
|
||||
Q_PROPERTY(QString configDirectory READ configDirectory WRITE setConfigDirectory NOTIFY configDirectoryChanged);
|
||||
/// The user to authenticate as. If unset the current user will be used.
|
||||
///
|
||||
/// This property may not be set while `active` is true.
|
||||
Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged);
|
||||
/// The last message sent by pam.
|
||||
Q_PROPERTY(QString message READ message NOTIFY messageChanged);
|
||||
/// If the last message should be shown as an error.
|
||||
Q_PROPERTY(bool messageIsError READ messageIsError NOTIFY messageIsErrorChanged);
|
||||
/// If pam currently wants a response.
|
||||
///
|
||||
/// Responses can be returned with the `respond()` function.
|
||||
Q_PROPERTY(bool responseRequired READ isResponseRequired NOTIFY responseRequiredChanged);
|
||||
// clang-format on
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit PamContext(QObject* parent = nullptr): QObject(parent) {}
|
||||
~PamContext() override;
|
||||
Q_DISABLE_COPY_MOVE(PamContext);
|
||||
|
||||
void classBegin() override {}
|
||||
void componentComplete() override;
|
||||
|
||||
void startConversation();
|
||||
void abortConversation();
|
||||
|
||||
/// Start an authentication session. Returns if the session was started successfully.
|
||||
Q_INVOKABLE bool start();
|
||||
|
||||
/// Abort a running authentication session.
|
||||
Q_INVOKABLE void abort();
|
||||
|
||||
/// Respond to pam.
|
||||
///
|
||||
/// May not be called unless `responseRequired` is true.
|
||||
Q_INVOKABLE void respond(QString response);
|
||||
|
||||
[[nodiscard]] bool isActive() const;
|
||||
void setActive(bool active);
|
||||
|
||||
[[nodiscard]] QString config() const;
|
||||
void setConfig(QString config);
|
||||
|
||||
[[nodiscard]] QString configDirectory() const;
|
||||
void setConfigDirectory(QString configDirectory);
|
||||
|
||||
[[nodiscard]] QString user() const;
|
||||
void setUser(QString user);
|
||||
|
||||
[[nodiscard]] QString message() const;
|
||||
[[nodiscard]] bool messageIsError() const;
|
||||
[[nodiscard]] bool isResponseRequired() const;
|
||||
|
||||
signals:
|
||||
/// Emitted whenever authentication completes.
|
||||
void completed(PamResult::Enum result);
|
||||
/// Emitted if pam fails to perform authentication normally.
|
||||
///
|
||||
/// A `completed(false)` will be emitted after this event.
|
||||
void error(PamError::Enum error);
|
||||
|
||||
/// Emitted whenever pam sends a new message, after the change signals for
|
||||
/// `message`, `messageIsError`, and `responseRequired`.
|
||||
void pamMessage();
|
||||
|
||||
void activeChanged();
|
||||
void configChanged();
|
||||
void configDirectoryChanged();
|
||||
void userChanged();
|
||||
void messageChanged();
|
||||
void messageIsErrorChanged();
|
||||
void responseRequiredChanged();
|
||||
|
||||
private slots:
|
||||
void onCompleted(PamResult::Enum result);
|
||||
void onError(PamError::Enum error);
|
||||
void onMessage(QString message, bool messageChanged, bool isError, bool responseRequired);
|
||||
|
||||
private:
|
||||
PamConversation* conversation = nullptr;
|
||||
|
||||
bool postInit = false;
|
||||
bool mTargetActive = false;
|
||||
QString mConfig = "login";
|
||||
QString mConfigDirectory = "/etc/pam.d";
|
||||
QString mUser;
|
||||
QString mMessage;
|
||||
bool mMessageIsError = false;
|
||||
bool mIsResponseRequired = false;
|
||||
};
|
Loading…
Reference in a new issue