forked from quickshell/quickshell
		
	service/pam: add pam service
This commit is contained in:
		
							parent
							
								
									f655875547
								
							
						
					
					
						commit
						7e5d128a91
					
				
					 11 changed files with 740 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue