service/polkit: add service module to write Polkit agents

This commit is contained in:
Cu3PO42 2025-10-09 23:50:08 +02:00 committed by outfoxxed
parent 1b147a2c78
commit 5ee8b62671
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
23 changed files with 1551 additions and 1 deletions

View file

@ -53,6 +53,7 @@ jobs:
libxcb \ libxcb \
libpipewire \ libpipewire \
cli11 \ cli11 \
polkit \
jemalloc jemalloc
- name: Build - name: Build

View file

@ -192,6 +192,13 @@ To disable: `-DSERVICE_PAM=OFF`
Dependencies: `pam` Dependencies: `pam`
### Polkit
This feature enables creating Polkit agents that can prompt user for authentication.
To disable: `-DSERVICE_POLKIT=OFF`
Dependencies: `polkit`, `glib`
### Hyprland ### Hyprland
This feature enables hyprland specific integrations. It requires wayland support This feature enables hyprland specific integrations. It requires wayland support
but has no extra dependencies. but has no extra dependencies.

View file

@ -67,6 +67,7 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON) boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON) boption(SERVICE_PAM "Pam" ON)
boption(SERVICE_POLKIT "Polkit" ON)
boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON)

View file

@ -11,6 +11,7 @@ set shell id.
## New Features ## New Features
- Added support for creating Polkit agents.
- Added support for creating wayland idle inhibitors. - Added support for creating wayland idle inhibitors.
- Added support for wayland idle timeouts. - Added support for wayland idle timeouts.
- Added the ability to override Quickshell.cacheDir with a custom path. - Added the ability to override Quickshell.cacheDir with a custom path.
@ -23,3 +24,7 @@ set shell id.
- Fixed volume control breaking with pipewire pro audio mode. - Fixed volume control breaking with pipewire pro audio mode.
- Fixed escape sequence handling in desktop entries. - Fixed escape sequence handling in desktop entries.
## Packaging Changes
`glib` and `polkit` have been added as dependencies when compiling with polkit agent support.

View file

@ -21,6 +21,8 @@
libgbm ? null, libgbm ? null,
pipewire, pipewire,
pam, pam,
polkit,
glib,
gitRev ? (let gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD; headExists = builtins.pathExists ./.git/HEAD;
@ -43,6 +45,7 @@
withPam ? true, withPam ? true,
withHyprland ? true, withHyprland ? true,
withI3 ? true, withI3 ? true,
withPolkit ? true,
}: let }: let
unwrapped = stdenv.mkDerivation { unwrapped = stdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}"; pname = "quickshell${lib.optionalString debug "-debug"}";
@ -76,7 +79,8 @@
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
++ lib.optional withX11 xorg.libxcb ++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam ++ lib.optional withPam pam
++ lib.optional withPipewire pipewire; ++ lib.optional withPipewire pipewire
++ lib.optionals withPolkit [ polkit glib ];
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
@ -91,6 +95,7 @@
(lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam) (lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "SERVICE_POLKIT" withPolkit)
(lib.cmakeBool "HYPRLAND" withHyprland) (lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3) (lib.cmakeBool "I3" withI3)
]; ];

View file

@ -42,6 +42,7 @@
libxcb libxcb
libxkbcommon libxkbcommon
linux-pam linux-pam
polkit
mesa mesa
pipewire pipewire
qtbase qtbase

View file

@ -14,6 +14,10 @@ if (SERVICE_PAM)
add_subdirectory(pam) add_subdirectory(pam)
endif() endif()
if (SERVICE_POLKIT)
add_subdirectory(polkit)
endif()
if (SERVICE_GREETD) if (SERVICE_GREETD)
add_subdirectory(greetd) add_subdirectory(greetd)
endif() endif()

View file

@ -0,0 +1,35 @@
find_package(PkgConfig REQUIRED)
pkg_check_modules(glib REQUIRED IMPORTED_TARGET glib-2.0>=2.36)
pkg_check_modules(gobject REQUIRED IMPORTED_TARGET gobject-2.0)
pkg_check_modules(polkit_agent REQUIRED IMPORTED_TARGET polkit-agent-1)
pkg_check_modules(polkit REQUIRED IMPORTED_TARGET polkit-gobject-1)
qt_add_library(quickshell-service-polkit STATIC
agentimpl.cpp
flow.cpp
identity.cpp
listener.cpp
session.cpp
qml.cpp
)
qt_add_qml_module(quickshell-service-polkit
URI Quickshell.Services.Polkit
VERSION 0.1
DEPENDENCIES QtQml
)
install_qml_module(quickshell-service-polkit)
target_link_libraries(quickshell-service-polkit PRIVATE
Qt::Qml
Qt::Quick
PkgConfig::glib
PkgConfig::gobject
PkgConfig::polkit_agent
PkgConfig::polkit
)
qs_module_pch(quickshell-service-polkit)
target_link_libraries(quickshell PRIVATE quickshell-service-polkitplugin)

View file

@ -0,0 +1,179 @@
#include "agentimpl.hpp"
#include <algorithm>
#include <utility>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include "../../core/generation.hpp"
#include "../../core/logcat.hpp"
#include "gobjectref.hpp"
#include "listener.hpp"
#include "qml.hpp"
namespace {
QS_LOGGING_CATEGORY(logPolkit, "quickshell.service.polkit", QtWarningMsg);
}
namespace qs::service::polkit {
PolkitAgentImpl* PolkitAgentImpl::instance = nullptr;
PolkitAgentImpl::PolkitAgentImpl(PolkitAgent* agent)
: QObject(nullptr)
, listener(qs_polkit_agent_new(this), G_OBJECT_NO_REF)
, qmlAgent(agent)
, path(this->qmlAgent->path()) {
auto utf8Path = this->path.toUtf8();
qs_polkit_agent_register(this->listener.get(), utf8Path.constData());
}
PolkitAgentImpl::~PolkitAgentImpl() { this->cancelAllRequests("PolkitAgent is being destroyed"); }
void PolkitAgentImpl::cancelAllRequests(const QString& reason) {
for (; !this->queuedRequests.empty(); this->queuedRequests.pop_back()) {
AuthRequest* req = this->queuedRequests.back();
qCDebug(logPolkit) << "destroying queued authentication request for action" << req->actionId;
req->cancel(reason);
delete req;
}
auto* flow = this->bActiveFlow.value();
if (flow) {
flow->cancelAuthenticationRequest();
flow->deleteLater();
}
if (this->bIsRegistered.value()) qs_polkit_agent_unregister(this->listener.get());
}
PolkitAgentImpl* PolkitAgentImpl::tryGetOrCreate(PolkitAgent* agent) {
if (instance == nullptr) instance = new PolkitAgentImpl(agent);
if (instance->qmlAgent == agent) return instance;
return nullptr;
}
PolkitAgentImpl* PolkitAgentImpl::tryGet(const PolkitAgent* agent) {
if (instance == nullptr) return nullptr;
if (instance->qmlAgent == agent) return instance;
return nullptr;
}
PolkitAgentImpl* PolkitAgentImpl::tryTakeoverOrCreate(PolkitAgent* agent) {
if (auto* impl = tryGetOrCreate(agent); impl != nullptr) return impl;
auto* prevGen = EngineGeneration::findObjectGeneration(instance->qmlAgent);
auto* myGen = EngineGeneration::findObjectGeneration(agent);
if (prevGen == myGen) return nullptr;
qCDebug(logPolkit) << "taking over listener from previous generation";
instance->qmlAgent = agent;
instance->setPath(agent->path());
return instance;
}
void PolkitAgentImpl::onEndOfQmlAgent(PolkitAgent* agent) {
if (instance != nullptr && instance->qmlAgent == agent) {
delete instance;
instance = nullptr;
}
}
void PolkitAgentImpl::setPath(const QString& path) {
if (this->path == path) return;
this->path = path;
auto utf8Path = path.toUtf8();
this->cancelAllRequests("PolkitAgent path changed");
qs_polkit_agent_unregister(this->listener.get());
this->bIsRegistered = false;
qs_polkit_agent_register(this->listener.get(), utf8Path.constData());
}
void PolkitAgentImpl::registerComplete(bool success) {
if (success) this->bIsRegistered = true;
else qCWarning(logPolkit) << "failed to register listener on path" << this->qmlAgent->path();
}
void PolkitAgentImpl::initiateAuthentication(AuthRequest* request) {
qCDebug(logPolkit) << "incoming authentication request for action" << request->actionId;
this->queuedRequests.emplace_back(request);
if (this->queuedRequests.size() == 1) {
this->activateAuthenticationRequest();
}
}
void PolkitAgentImpl::cancelAuthentication(AuthRequest* request) {
qCDebug(logPolkit) << "cancelling authentication request from agent";
auto* flow = this->bActiveFlow.value();
if (flow && flow->authRequest() == request) {
flow->cancelFromAgent();
} else if (auto it = std::ranges::find(this->queuedRequests, request);
it != this->queuedRequests.end())
{
qCDebug(logPolkit) << "removing queued authentication request for action" << (*it)->actionId;
(*it)->cancel("Authentication request was cancelled");
delete (*it);
this->queuedRequests.erase(it);
} else {
qCWarning(logPolkit) << "the cancelled request was not found in the queue.";
}
}
void PolkitAgentImpl::activateAuthenticationRequest() {
if (this->queuedRequests.empty()) return;
AuthRequest* req = this->queuedRequests.front();
this->queuedRequests.pop_front();
qCDebug(logPolkit) << "activating authentication request for action" << req->actionId
<< ", cookie: " << req->cookie;
QList<Identity*> identities;
for (auto& identity: req->identities) {
auto* obj = Identity::fromPolkitIdentity(identity);
if (obj) identities.append(obj);
}
if (identities.isEmpty()) {
qCWarning(logPolkit
) << "no supported identities available for authentication request, cancelling.";
req->cancel("Error requesting authentication: no supported identities available.");
delete req;
return;
}
this->bActiveFlow = new AuthFlow(req, std::move(identities));
QObject::connect(
this->bActiveFlow.value(),
&AuthFlow::isCompletedChanged,
this,
&PolkitAgentImpl::finishAuthenticationRequest
);
emit this->qmlAgent->authenticationRequestStarted();
}
void PolkitAgentImpl::finishAuthenticationRequest() {
if (!this->bActiveFlow.value()) return;
qCDebug(logPolkit) << "finishing authentication request for action"
<< this->bActiveFlow.value()->actionId();
this->bActiveFlow.value()->deleteLater();
if (!this->queuedRequests.empty()) {
this->activateAuthenticationRequest();
} else {
this->bActiveFlow = nullptr;
}
}
} // namespace qs::service::polkit

View file

@ -0,0 +1,66 @@
#pragma once
#include <deque>
#include <qobject.h>
#include <qproperty.h>
#include "flow.hpp"
#include "gobjectref.hpp"
#include "listener.hpp"
namespace qs::service::polkit {
class PolkitAgent;
class PolkitAgentImpl
: public QObject
, public ListenerCb {
Q_OBJECT;
Q_DISABLE_COPY_MOVE(PolkitAgentImpl);
public:
~PolkitAgentImpl() override;
static PolkitAgentImpl* tryGetOrCreate(PolkitAgent* agent);
static PolkitAgentImpl* tryGet(const PolkitAgent* agent);
static PolkitAgentImpl* tryTakeoverOrCreate(PolkitAgent* agent);
static void onEndOfQmlAgent(PolkitAgent* agent);
[[nodiscard]] QBindable<AuthFlow*> activeFlow() { return &this->bActiveFlow; };
[[nodiscard]] QBindable<bool> isRegistered() { return &this->bIsRegistered; };
[[nodiscard]] const QString& getPath() const { return this->path; }
void setPath(const QString& path);
void initiateAuthentication(AuthRequest* request) override;
void cancelAuthentication(AuthRequest* request) override;
void registerComplete(bool success) override;
void cancelAllRequests(const QString& reason);
signals:
void activeFlowChanged();
void isRegisteredChanged();
private:
PolkitAgentImpl(PolkitAgent* agent);
static PolkitAgentImpl* instance;
/// Start handling of the next authentication request in the queue.
void activateAuthenticationRequest();
/// Finalize and remove the current authentication request.
void finishAuthenticationRequest();
GObjectRef<QsPolkitAgent> listener;
PolkitAgent* qmlAgent = nullptr;
QString path;
std::deque<AuthRequest*> queuedRequests;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(PolkitAgentImpl, AuthFlow*, bActiveFlow, &PolkitAgentImpl::activeFlowChanged);
Q_OBJECT_BINDABLE_PROPERTY(PolkitAgentImpl, bool, bIsRegistered, &PolkitAgentImpl::isRegisteredChanged);
// clang-format on
};
} // namespace qs::service::polkit

View file

@ -0,0 +1,163 @@
#include "flow.hpp"
#include <utility>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlinfo.h>
#include <qtmetamacros.h>
#include "../../core/logcat.hpp"
#include "identity.hpp"
#include "qml.hpp"
#include "session.hpp"
namespace {
QS_LOGGING_CATEGORY(logPolkitState, "quickshell.service.polkit.state", QtWarningMsg);
}
namespace qs::service::polkit {
AuthFlow::AuthFlow(AuthRequest* request, QList<Identity*>&& identities, QObject* parent)
: QObject(parent)
, mRequest(request)
, mIdentities(std::move(identities))
, bSelectedIdentity(this->mIdentities.isEmpty() ? nullptr : this->mIdentities.first()) {
// We reject auth requests with no identities before a flow is created.
// This should never happen.
if (!this->bSelectedIdentity.value())
qCFatal(logPolkitState) << "AuthFlow created with no valid identities!";
for (auto* identity: this->mIdentities) {
identity->setParent(this);
}
this->setupSession();
}
AuthFlow::~AuthFlow() { delete this->mRequest; };
void AuthFlow::setSelectedIdentity(Identity* identity) {
if (this->bSelectedIdentity.value() == identity) return;
if (!identity) {
qmlWarning(this) << "Cannot set selected identity to null.";
return;
}
this->bSelectedIdentity = identity;
this->currentSession->cancel();
this->setupSession();
}
void AuthFlow::cancelFromAgent() {
if (!this->currentSession) return;
qCDebug(logPolkitState) << "cancelling authentication request from agent";
// Session cancel can immediately call the cancel handler, which also
// performs property updates.
Qt::beginPropertyUpdateGroup();
this->bIsCancelled = true;
this->currentSession->cancel();
Qt::endPropertyUpdateGroup();
emit this->authenticationRequestCancelled();
this->mRequest->cancel("Authentication request cancelled by agent.");
}
void AuthFlow::submit(const QString& value) {
if (!this->currentSession) return;
qCDebug(logPolkitState) << "submitting response to authentication request";
this->currentSession->respond(value);
Qt::beginPropertyUpdateGroup();
this->bIsResponseRequired = false;
this->bInputPrompt = QString();
this->bResponseVisible = false;
Qt::endPropertyUpdateGroup();
}
void AuthFlow::cancelAuthenticationRequest() {
if (!this->currentSession) return;
qCDebug(logPolkitState) << "cancelling authentication request by user request";
// Session cancel can immediately call the cancel handler, which also
// performs property updates.
Qt::beginPropertyUpdateGroup();
this->bIsCancelled = true;
this->currentSession->cancel();
Qt::endPropertyUpdateGroup();
this->mRequest->cancel("Authentication request cancelled by user.");
}
void AuthFlow::setupSession() {
delete this->currentSession;
qCDebug(logPolkitState) << "setting up session for identity"
<< this->bSelectedIdentity.value()->name();
this->currentSession = new Session(
this->bSelectedIdentity.value()->polkitIdentity.get(),
this->mRequest->cookie,
this
);
QObject::connect(this->currentSession, &Session::request, this, &AuthFlow::request);
QObject::connect(this->currentSession, &Session::completed, this, &AuthFlow::completed);
QObject::connect(this->currentSession, &Session::showError, this, &AuthFlow::showError);
QObject::connect(this->currentSession, &Session::showInfo, this, &AuthFlow::showInfo);
this->currentSession->initiate();
}
void AuthFlow::request(const QString& message, bool echo) {
Qt::beginPropertyUpdateGroup();
this->bIsResponseRequired = true;
this->bInputPrompt = message;
this->bResponseVisible = echo;
Qt::endPropertyUpdateGroup();
}
void AuthFlow::completed(bool gainedAuthorization) {
qCDebug(logPolkitState) << "authentication session completed, gainedAuthorization ="
<< gainedAuthorization << ", isCancelled =" << this->bIsCancelled.value();
if (gainedAuthorization) {
Qt::beginPropertyUpdateGroup();
this->bIsCompleted = true;
this->bIsSuccessful = true;
Qt::endPropertyUpdateGroup();
this->mRequest->complete();
emit this->authenticationSucceeded();
} else if (this->bIsCancelled.value()) {
Qt::beginPropertyUpdateGroup();
this->bIsCompleted = true;
this->bIsSuccessful = false;
Qt::endPropertyUpdateGroup();
} else {
this->bFailed = true;
emit this->authenticationFailed();
this->setupSession();
}
}
void AuthFlow::showError(const QString& message) {
Qt::beginPropertyUpdateGroup();
this->bSupplementaryMessage = message;
this->bSupplementaryIsError = true;
Qt::endPropertyUpdateGroup();
}
void AuthFlow::showInfo(const QString& message) {
Qt::beginPropertyUpdateGroup();
this->bSupplementaryMessage = message;
this->bSupplementaryIsError = false;
Qt::endPropertyUpdateGroup();
}
} // namespace qs::service::polkit

View file

@ -0,0 +1,179 @@
#pragma once
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include "../../core/retainable.hpp"
#include "identity.hpp"
#include "listener.hpp"
namespace qs::service::polkit {
class Session;
class AuthFlow
: public QObject
, public Retainable {
Q_OBJECT;
QML_ELEMENT;
Q_DISABLE_COPY_MOVE(AuthFlow);
QML_UNCREATABLE("AuthFlow can only be obtained from PolkitAgent.");
// clang-format off
/// The main message to present to the user.
Q_PROPERTY(QString message READ message CONSTANT);
/// The icon to present to the user in association with the message.
///
/// The icon name follows the [FreeDesktop icon naming specification](https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html).
/// Use @@Quickshell.Quickshell.iconPath() to resolve the icon name to an
/// actual file path for display.
Q_PROPERTY(QString iconName READ iconName CONSTANT);
/// The action ID represents the action that is being authorized.
///
/// This is a machine-readable identifier.
Q_PROPERTY(QString actionId READ actionId CONSTANT);
/// A cookie that identifies this authentication request.
///
/// This is an internal identifier and not recommended to show to users.
Q_PROPERTY(QString cookie READ cookie CONSTANT);
/// The list of identities that may be used to authenticate.
///
/// Each identity may be a user or a group. You may select any of them to
/// authenticate by setting @@selectedIdentity. By default, the first identity
/// in the list is selected.
Q_PROPERTY(QList<Identity*> identities READ identities CONSTANT);
/// The identity that will be used to authenticate.
///
/// Changing this will abort any ongoing authentication conversations and start a new one.
Q_PROPERTY(Identity* selectedIdentity READ default WRITE setSelectedIdentity NOTIFY selectedIdentityChanged BINDABLE selectedIdentity);
/// Indicates that a response from the user is required from the user,
/// typically a password.
Q_PROPERTY(bool isResponseRequired READ default NOTIFY isResponseRequiredChanged BINDABLE isResponseRequired);
/// This message is used to prompt the user for required input.
Q_PROPERTY(QString inputPrompt READ default NOTIFY inputPromptChanged BINDABLE inputPrompt);
/// Indicates whether the user's response should be visible. (e.g. for passwords this should be false)
Q_PROPERTY(bool responseVisible READ default NOTIFY responseVisibleChanged BINDABLE responseVisible);
/// An additional message to present to the user.
///
/// This may be used to show errors or supplementary information.
/// See @@supplementaryIsError to determine if this is an error message.
Q_PROPERTY(QString supplementaryMessage READ default NOTIFY supplementaryMessageChanged BINDABLE supplementaryMessage);
/// Indicates whether the supplementary message is an error.
Q_PROPERTY(bool supplementaryIsError READ default NOTIFY supplementaryIsErrorChanged BINDABLE supplementaryIsError);
/// Has the authentication request been completed.
Q_PROPERTY(bool isCompleted READ default NOTIFY isCompletedChanged BINDABLE isCompleted);
/// Indicates whether the authentication request was successful.
Q_PROPERTY(bool isSuccessful READ default NOTIFY isSuccessfulChanged BINDABLE isSuccessful);
/// Indicates whether the current authentication request was cancelled.
Q_PROPERTY(bool isCancelled READ default NOTIFY isCancelledChanged BINDABLE isCancelled);
/// Indicates whether an authentication attempt has failed at least once during this authentication flow.
Q_PROPERTY(bool failed READ default NOTIFY failedChanged BINDABLE failed);
// clang-format on
public:
explicit AuthFlow(AuthRequest* request, QList<Identity*>&& identities, QObject* parent = nullptr);
~AuthFlow() override;
/// Cancel the ongoing authentication request from the agent side.
void cancelFromAgent();
/// Submit a response to a request that was previously emitted. Typically the password.
Q_INVOKABLE void submit(const QString& value);
/// Cancel the ongoing authentication request from the user side.
Q_INVOKABLE void cancelAuthenticationRequest();
[[nodiscard]] const QString& message() const { return this->mRequest->message; };
[[nodiscard]] const QString& iconName() const { return this->mRequest->iconName; };
[[nodiscard]] const QString& actionId() const { return this->mRequest->actionId; };
[[nodiscard]] const QString& cookie() const { return this->mRequest->cookie; };
[[nodiscard]] const QList<Identity*>& identities() const { return this->mIdentities; };
[[nodiscard]] QBindable<Identity*> selectedIdentity() { return &this->bSelectedIdentity; };
void setSelectedIdentity(Identity* identity);
[[nodiscard]] QBindable<bool> isResponseRequired() { return &this->bIsResponseRequired; };
[[nodiscard]] QBindable<QString> inputPrompt() { return &this->bInputPrompt; };
[[nodiscard]] QBindable<bool> responseVisible() { return &this->bResponseVisible; };
[[nodiscard]] QBindable<QString> supplementaryMessage() { return &this->bSupplementaryMessage; };
[[nodiscard]] QBindable<bool> supplementaryIsError() { return &this->bSupplementaryIsError; };
[[nodiscard]] QBindable<bool> isCompleted() { return &this->bIsCompleted; };
[[nodiscard]] QBindable<bool> isSuccessful() { return &this->bIsSuccessful; };
[[nodiscard]] QBindable<bool> isCancelled() { return &this->bIsCancelled; };
[[nodiscard]] QBindable<bool> failed() { return &this->bFailed; };
[[nodiscard]] AuthRequest* authRequest() const { return this->mRequest; };
signals:
/// Emitted whenever an authentication request completes successfully.
void authenticationSucceeded();
/// Emitted whenever an authentication request completes unsuccessfully.
///
/// This may be because the user entered the wrong password or otherwise
/// failed to authenticate.
/// This signal is not emmitted when the user canceled the request or it
/// was cancelled by the PolKit daemon.
///
/// After this signal, a new session is automatically started for the same
/// identity.
void authenticationFailed();
/// Emmitted when on ongoing authentication request is cancelled by the PolKit daemon.
void authenticationRequestCancelled();
void selectedIdentityChanged();
void isResponseRequiredChanged();
void inputPromptChanged();
void responseVisibleChanged();
void supplementaryMessageChanged();
void supplementaryIsErrorChanged();
void isCompletedChanged();
void isSuccessfulChanged();
void isCancelledChanged();
void failedChanged();
private slots:
// Signals received from session objects.
void request(const QString& message, bool echo);
void completed(bool gainedAuthorization);
void showError(const QString& message);
void showInfo(const QString& message);
private:
/// Start a session for the currently selected identity and the current request.
void setupSession();
Session* currentSession = nullptr;
AuthRequest* mRequest = nullptr;
QList<Identity*> mIdentities;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, Identity*, bSelectedIdentity, &AuthFlow::selectedIdentityChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsResponseRequired, &AuthFlow::isResponseRequiredChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, QString, bInputPrompt, &AuthFlow::inputPromptChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bResponseVisible, &AuthFlow::responseVisibleChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, QString, bSupplementaryMessage, &AuthFlow::supplementaryMessageChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bSupplementaryIsError, &AuthFlow::supplementaryIsErrorChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsCompleted, &AuthFlow::isCompletedChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsSuccessful, &AuthFlow::isSuccessfulChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsCancelled, &AuthFlow::isCancelledChanged);
Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bFailed, &AuthFlow::failedChanged);
// clang-format on
};
} // namespace qs::service::polkit

View file

@ -0,0 +1,65 @@
#pragma once
#include <glib-object.h>
namespace qs::service::polkit {
struct GObjectNoRefTag {};
constexpr GObjectNoRefTag G_OBJECT_NO_REF;
template <typename T>
class GObjectRef {
public:
explicit GObjectRef(T* ptr = nullptr): ptr(ptr) {
if (this->ptr) {
g_object_ref(this->ptr);
}
}
explicit GObjectRef(T* ptr, GObjectNoRefTag /*tag*/): ptr(ptr) {}
~GObjectRef() {
if (this->ptr) {
g_object_unref(this->ptr);
}
}
// We do handle self-assignment in a more general case by checking the
// included pointers rather than the wrapper objects themselves.
// NOLINTBEGIN(bugprone-unhandled-self-assignment)
GObjectRef(const GObjectRef& other): GObjectRef(other.ptr) {}
GObjectRef& operator=(const GObjectRef& other) {
if (*this == other) return *this;
if (this->ptr) {
g_object_unref(this->ptr);
}
this->ptr = other.ptr;
if (this->ptr) {
g_object_ref(this->ptr);
}
return *this;
}
GObjectRef(GObjectRef&& other) noexcept: ptr(other.ptr) { other.ptr = nullptr; }
GObjectRef& operator=(GObjectRef&& other) noexcept {
if (*this == other) return *this;
if (this->ptr) {
g_object_unref(this->ptr);
}
this->ptr = other.ptr;
other.ptr = nullptr;
return *this;
}
// NOLINTEND(bugprone-unhandled-self-assignment)
[[nodiscard]] T* get() const { return this->ptr; }
T* operator->() const { return this->ptr; }
bool operator==(const GObjectRef<T>& other) const { return this->ptr == other.ptr; }
private:
T* ptr;
};
} // namespace qs::service::polkit

View file

@ -0,0 +1,84 @@
#include "identity.hpp"
#include <type_traits>
#include <utility>
#include <vector>
#include <qobject.h>
#include <qtmetamacros.h>
#include <sys/types.h>
#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE
// Workaround macro collision with glib 'signals' struct member.
#undef signals
#include <polkit/polkit.h>
#define signals Q_SIGNALS
#include <grp.h>
#include <pwd.h>
#include <unistd.h>
#include "gobjectref.hpp"
namespace qs::service::polkit {
Identity::Identity(
id_t id,
QString name,
QString displayName,
bool isGroup,
GObjectRef<PolkitIdentity> polkitIdentity,
QObject* parent
)
: QObject(parent)
, polkitIdentity(std::move(polkitIdentity))
, mId(id)
, mName(std::move(name))
, mDisplayName(std::move(displayName))
, mIsGroup(isGroup) {}
Identity* Identity::fromPolkitIdentity(GObjectRef<PolkitIdentity> identity) {
if (POLKIT_IS_UNIX_USER(identity.get())) {
auto uid = polkit_unix_user_get_uid(POLKIT_UNIX_USER(identity.get()));
auto bufSize = sysconf(_SC_GETPW_R_SIZE_MAX);
// The call can fail with -1, in this case choose a default that is
// big enough.
if (bufSize == -1) bufSize = 16384;
auto buffer = std::vector<char>(bufSize);
std::aligned_storage_t<sizeof(passwd), alignof(passwd)> pwBuf;
passwd* pw = nullptr;
getpwuid_r(uid, reinterpret_cast<passwd*>(&pwBuf), buffer.data(), bufSize, &pw);
auto name =
(pw && pw->pw_name && *pw->pw_name) ? QString::fromUtf8(pw->pw_name) : QString::number(uid);
return new Identity(
uid,
name,
(pw && pw->pw_gecos && *pw->pw_gecos) ? QString::fromUtf8(pw->pw_gecos) : name,
false,
std::move(identity)
);
}
if (POLKIT_IS_UNIX_GROUP(identity.get())) {
auto gid = polkit_unix_group_get_gid(POLKIT_UNIX_GROUP(identity.get()));
auto bufSize = sysconf(_SC_GETGR_R_SIZE_MAX);
// The call can fail with -1, in this case choose a default that is
// big enough.
if (bufSize == -1) bufSize = 16384;
auto buffer = std::vector<char>(bufSize);
std::aligned_storage_t<sizeof(group), alignof(group)> grBuf;
group* gr = nullptr;
getgrgid_r(gid, reinterpret_cast<group*>(&grBuf), buffer.data(), bufSize, &gr);
auto name =
(gr && gr->gr_name && *gr->gr_name) ? QString::fromUtf8(gr->gr_name) : QString::number(gid);
return new Identity(gid, name, name, true, std::move(identity));
}
// A different type of identity is netgroup.
return nullptr;
}
} // namespace qs::service::polkit

View file

@ -0,0 +1,64 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include "gobjectref.hpp"
// _PolkitIdentity is considered a reserved identifier, but I am specifically
// forward declaring this reserved name.
using PolkitIdentity = struct _PolkitIdentity; // NOLINT(bugprone-reserved-identifier)
namespace qs::service::polkit {
//! Represents a user or group that can be used to authenticate.
class Identity: public QObject {
Q_OBJECT;
Q_DISABLE_COPY_MOVE(Identity);
// clang-format off
/// The Id of the identity. If the identity is a user, this is the user's uid. See @@isGroup.
Q_PROPERTY(quint32 id READ id CONSTANT);
/// The name of the user or group.
///
/// If available, this is the actual username or group name, but may fallback to the ID.
Q_PROPERTY(QString string READ name CONSTANT);
/// The full name of the user or group, if available. Otherwise the same as @@name.
Q_PROPERTY(QString displayName READ displayName CONSTANT);
/// Indicates if this identity is a group or a user.
///
/// If true, @@id is a gid, otherwise it is a uid.
Q_PROPERTY(bool isGroup READ isGroup CONSTANT);
QML_UNCREATABLE("Identities cannot be created directly.");
// clang-format on
public:
explicit Identity(
id_t id,
QString name,
QString displayName,
bool isGroup,
GObjectRef<PolkitIdentity> polkitIdentity,
QObject* parent = nullptr
);
~Identity() override = default;
static Identity* fromPolkitIdentity(GObjectRef<PolkitIdentity> identity);
[[nodiscard]] quint32 id() const { return static_cast<quint32>(this->mId); };
[[nodiscard]] const QString& name() const { return this->mName; };
[[nodiscard]] const QString& displayName() const { return this->mDisplayName; };
[[nodiscard]] bool isGroup() const { return this->mIsGroup; };
GObjectRef<PolkitIdentity> polkitIdentity;
private:
id_t mId;
QString mName;
QString mDisplayName;
bool mIsGroup;
};
} // namespace qs::service::polkit

View file

@ -0,0 +1,234 @@
#include "listener.hpp"
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include <gio/gio.h>
#include <glib-object.h>
#include <glib.h>
#include <polkit/polkit.h>
#include <polkit/polkittypes.h>
#include <polkitagent/polkitagent.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <unistd.h>
#include "../../core/logcat.hpp"
#include "gobjectref.hpp"
#include "qml.hpp"
namespace {
QS_LOGGING_CATEGORY(logPolkitListener, "quickshell.service.polkit.listener", QtWarningMsg);
}
using qs::service::polkit::GObjectRef;
// This is mostly GObject code, we follow their naming conventions for improved
// clarity and to mark it as such. Additionally, many methods need to be static
// to conform with the expected declarations.
// NOLINTBEGIN(readability-identifier-naming,misc-use-anonymous-namespace)
using QsPolkitAgent = struct _QsPolkitAgent {
PolkitAgentListener parent_instance;
qs::service::polkit::ListenerCb* cb;
gpointer registration_handle;
};
G_DEFINE_TYPE(QsPolkitAgent, qs_polkit_agent, POLKIT_AGENT_TYPE_LISTENER)
static void initiate_authentication(
PolkitAgentListener* listener,
const gchar* actionId,
const gchar* message,
const gchar* iconName,
PolkitDetails* details,
const gchar* cookie,
GList* identities,
GCancellable* cancellable,
GAsyncReadyCallback callback,
gpointer userData
);
static gboolean
initiate_authentication_finish(PolkitAgentListener* listener, GAsyncResult* result, GError** error);
static void qs_polkit_agent_init(QsPolkitAgent* self) {
self->cb = nullptr;
self->registration_handle = nullptr;
}
static void qs_polkit_agent_finalize(GObject* object) {
if (G_OBJECT_CLASS(qs_polkit_agent_parent_class))
G_OBJECT_CLASS(qs_polkit_agent_parent_class)->finalize(object);
}
static void qs_polkit_agent_class_init(QsPolkitAgentClass* klass) {
GObjectClass* gobject_class = G_OBJECT_CLASS(klass);
gobject_class->finalize = qs_polkit_agent_finalize;
PolkitAgentListenerClass* listener_class = POLKIT_AGENT_LISTENER_CLASS(klass);
listener_class->initiate_authentication = initiate_authentication;
listener_class->initiate_authentication_finish = initiate_authentication_finish;
}
QsPolkitAgent* qs_polkit_agent_new(qs::service::polkit::ListenerCb* cb) {
QsPolkitAgent* self = QS_POLKIT_AGENT(g_object_new(QS_TYPE_POLKIT_AGENT, nullptr));
self->cb = cb;
return self;
}
struct RegisterCbData {
GObjectRef<QsPolkitAgent> agent;
std::string path;
};
static void qs_polkit_agent_register_cb(GObject* /*unused*/, GAsyncResult* res, gpointer userData);
void qs_polkit_agent_register(QsPolkitAgent* agent, const char* path) {
if (path == nullptr || *path == '\0') {
qCWarning(logPolkitListener) << "cannot register listener without a path set.";
agent->cb->registerComplete(false);
return;
}
auto* data = new RegisterCbData {.agent = GObjectRef(agent), .path = path};
polkit_unix_session_new_for_process(getpid(), nullptr, &qs_polkit_agent_register_cb, data);
}
static void qs_polkit_agent_register_cb(GObject* /*unused*/, GAsyncResult* res, gpointer userData) {
std::unique_ptr<RegisterCbData> data(reinterpret_cast<RegisterCbData*>(userData));
GError* error = nullptr;
auto* subject = polkit_unix_session_new_for_process_finish(res, &error);
if (subject == nullptr || error != nullptr) {
qCWarning(logPolkitListener) << "failed to create subject for listener:"
<< (error ? error->message : "<unknown error>");
g_clear_error(&error);
data->agent->cb->registerComplete(false);
return;
}
data->agent->registration_handle = polkit_agent_listener_register(
POLKIT_AGENT_LISTENER(data->agent.get()),
POLKIT_AGENT_REGISTER_FLAGS_NONE,
subject,
data->path.c_str(),
nullptr,
&error
);
g_object_unref(subject);
if (error != nullptr) {
qCWarning(logPolkitListener) << "failed to register listener:" << error->message;
g_clear_error(&error);
data->agent->cb->registerComplete(false);
return;
}
data->agent->cb->registerComplete(true);
}
void qs_polkit_agent_unregister(QsPolkitAgent* agent) {
if (agent->registration_handle != nullptr) {
polkit_agent_listener_unregister(agent->registration_handle);
agent->registration_handle = nullptr;
}
}
static void authentication_cancelled_cb(GCancellable* /*unused*/, gpointer userData) {
auto* request = static_cast<qs::service::polkit::AuthRequest*>(userData);
request->cb->cancelAuthentication(request);
}
static void initiate_authentication(
PolkitAgentListener* listener,
const gchar* actionId,
const gchar* message,
const gchar* iconName,
PolkitDetails* /*unused*/,
const gchar* cookie,
GList* identities,
GCancellable* cancellable,
GAsyncReadyCallback callback,
gpointer userData
) {
auto* self = QS_POLKIT_AGENT(listener);
auto* asyncResult = g_task_new(reinterpret_cast<GObject*>(self), nullptr, callback, userData);
// Identities may be duplicated, so we use the hash to filter them out.
std::unordered_set<guint> identitySet;
std::vector<GObjectRef<PolkitIdentity>> identityVector;
for (auto* item = g_list_first(identities); item != nullptr; item = g_list_next(item)) {
auto* identity = static_cast<PolkitIdentity*>(item->data);
if (identitySet.contains(polkit_identity_hash(identity))) continue;
identitySet.insert(polkit_identity_hash(identity));
// The caller unrefs all identities after we return, therefore we need to
// take our own reference for the identities we keep. Our wrapper does
// this automatically.
identityVector.emplace_back(identity);
}
// The original strings are freed by the caller after we return, so we
// copy them into QStrings.
auto* request = new qs::service::polkit::AuthRequest {
.actionId = QString::fromUtf8(actionId),
.message = QString::fromUtf8(message),
.iconName = QString::fromUtf8(iconName),
.cookie = QString::fromUtf8(cookie),
.identities = std::move(identityVector),
.task = asyncResult,
.cancellable = cancellable,
.handlerId = 0,
.cb = self->cb
};
if (cancellable != nullptr) {
request->handlerId = g_cancellable_connect(
cancellable,
reinterpret_cast<GCallback>(authentication_cancelled_cb),
request,
nullptr
);
}
self->cb->initiateAuthentication(request);
}
static gboolean initiate_authentication_finish(
PolkitAgentListener* /*unused*/,
GAsyncResult* result,
GError** error
) {
return g_task_propagate_boolean(G_TASK(result), error);
}
namespace qs::service::polkit {
// While these functions can be const since they do not modify member variables,
// they are logically non-const since they modify the state of the
// authentication request. Therefore, we do not mark them as const.
// NOLINTBEGIN(readability-make-member-function-const)
void AuthRequest::complete() { g_task_return_boolean(this->task, true); }
void AuthRequest::cancel(const QString& reason) {
auto utf8Reason = reason.toUtf8();
g_task_return_new_error(
this->task,
POLKIT_ERROR,
POLKIT_ERROR_CANCELLED,
"%s",
utf8Reason.constData()
);
}
// NOLINTEND(readability-make-member-function-const)
} // namespace qs::service::polkit
// NOLINTEND(readability-identifier-naming,misc-use-anonymous-namespace)

View file

@ -0,0 +1,75 @@
#pragma once
#include <qstring.h>
#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE
// This causes a problem with variables of the name.
#undef signals
#include <glib-object.h>
#include <polkitagent/polkitagent.h>
#define signals Q_SIGNALS
#include "gobjectref.hpp"
namespace qs::service::polkit {
class ListenerCb;
//! All state that comes in from PolKit about an authentication request.
struct AuthRequest {
//! The action ID that this session is for.
QString actionId;
//! Message to present to the user.
QString message;
//! Icon name according to the FreeDesktop specification. May be empty.
QString iconName;
// Details intentionally omitted because nothing seems to use them.
QString cookie;
//! List of users/groups that can be used for authentication.
std::vector<GObjectRef<PolkitIdentity>> identities;
//! Implementation detail to mark authentication done.
GTask* task;
//! Implementation detail for requests cancelled by agent.
GCancellable* cancellable;
//! Callback handler ID for the cancellable.
gulong handlerId;
//! Callbacks for the listener
ListenerCb* cb;
void complete();
void cancel(const QString& reason);
};
//! Callback interface for PolkitAgent listener events.
class ListenerCb {
public:
ListenerCb() = default;
virtual ~ListenerCb() = default;
Q_DISABLE_COPY_MOVE(ListenerCb);
//! Called when the agent registration is complete.
virtual void registerComplete(bool success) = 0;
//! Called when an authentication request is initiated by PolKit.
virtual void initiateAuthentication(AuthRequest* request) = 0;
//! Called when an authentication request is cancelled by PolKit before completion.
virtual void cancelAuthentication(AuthRequest* request) = 0;
};
} // namespace qs::service::polkit
G_BEGIN_DECLS
// This is GObject code. By using their naming conventions, we clearly mark it
// as such for the rest of the project.
// NOLINTBEGIN(readability-identifier-naming)
#define QS_TYPE_POLKIT_AGENT (qs_polkit_agent_get_type())
G_DECLARE_FINAL_TYPE(QsPolkitAgent, qs_polkit_agent, QS, POLKIT_AGENT, PolkitAgentListener)
QsPolkitAgent* qs_polkit_agent_new(qs::service::polkit::ListenerCb* cb);
void qs_polkit_agent_register(QsPolkitAgent* agent, const char* path);
void qs_polkit_agent_unregister(QsPolkitAgent* agent);
// NOLINTEND(readability-identifier-naming)
G_END_DECLS

View file

@ -0,0 +1,46 @@
name = "Quickshell.Services.Polkit"
description = "Polkit API"
headers = [agentimpl.hpp, flow.hpp, identity.hpp, listener.hpp, qml.hpp, session.hpp]
-----
## Purpose of a Polkit Agent
PolKit is a system for privileged applications to query if a user is permitted to execute an action.
You have probably seen it in the form of a "Please enter your password to continue with X" dialog box before.
This dialog box is presented by your *PolKit agent*, it is a process running as your user that accepts authentication requests from the *daemon* and presents them to you to accept or deny.
This service enables writing a PolKit agent in Quickshell.
## Implementing a Polkit Agent
The backend logic of communicating with the daemon is handled by the @@PolkitAgent object.
It exposes incoming requests via @@PolkitAgent.flow and provides appropriate signals.
### Flow of an authentication request
Incoming authentication requests are queued in the order that they arrive.
If none is queued, a request starts processing right away.
Otherwise, it will wait until prior requests are done.
A request starts by emitting the @@PolkitAgent.authenticationRequestStarted signal.
At this point, information like the action to be performed and permitted users that can authenticate is available.
An authentication *session* for the request is immediately started, which internally starts a PAM conversation that is likely to prompt for user input.
* Additional prompts may be shared with the user by way of the @@AuthFlow.supplementaryMessageChanged / @@AuthFlow.supplementaryIsErrorChanged signals and the @@AuthFlow.supplementaryMessage and @@AuthFlow.supplementaryIsError properties. A common message might be 'Please input your password'.
* An input request is forwarded via the @@AuthFlow.isResponseRequiredChanged / @@AuthFlow.inputPromptChanged / @@AuthFlow.responseVisibleChanged signals and the corresponding properties. Note that the request specifies whether the text box should show the typed input on screen or replace it with placeholders.
User replies can be submitted via the @@AuthFlow.submit method.
A conversation can take multiple turns, for example if second factors are involved.
If authentication fails, we automatically create a fresh session so the user can try again.
The @@AuthFlow.authenticationFailed signal is emitted in this case.
If authentication is successful, you receive the @@AuthFlow.authenticationSucceeeded signal. At this point, the dialog can be closed.
If additional requests are queued, you will receive the @@PolkitAgent.authenticationRequestStarted signal again.
#### Cancelled requests
Requests may either be canceled by the user or the PolKit daemon.
In this case, we clean up any state and proceed to the next request, if any.
If the request was cancelled by the daemon and not the user, you also receive the @@AuthFlow.authenticationRequestCancelled signal.

View file

@ -0,0 +1,35 @@
#include "qml.hpp"
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include "../../core/logcat.hpp"
#include "agentimpl.hpp"
namespace {
QS_LOGGING_CATEGORY(logPolkit, "quickshell.service.polkit", QtWarningMsg);
}
namespace qs::service::polkit {
PolkitAgent::~PolkitAgent() { PolkitAgentImpl::onEndOfQmlAgent(this); };
void PolkitAgent::componentComplete() {
if (this->mPath.isEmpty()) this->mPath = "/org/quickshell/PolkitAgent";
auto* impl = PolkitAgentImpl::tryTakeoverOrCreate(this);
if (impl == nullptr) return;
this->bFlow.setBinding([impl]() { return impl->activeFlow().value(); });
this->bIsActive.setBinding([impl]() { return impl->activeFlow().value() != nullptr; });
this->bIsRegistered.setBinding([impl]() { return impl->isRegistered().value(); });
}
void PolkitAgent::setPath(const QString& path) {
if (this->mPath.isEmpty()) {
this->mPath = path;
} else if (this->mPath != path) {
qCWarning(logPolkit) << "cannot change path after it has been set.";
}
}
} // namespace qs::service::polkit

View file

@ -0,0 +1,84 @@
#pragma once
#include <deque>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qqmlparserstatus.h>
#include <sys/types.h>
#include "flow.hpp"
// The reserved identifier is exactly the struct I mean.
using PolkitIdentity = struct _PolkitIdentity; // NOLINT(bugprone-reserved-identifier)
using QsPolkitAgent = struct _QsPolkitAgent;
namespace qs::service::polkit {
struct AuthRequest;
class Session;
class Identity;
class AuthFlow;
//! Contains interface to instantiate a PolKit agent listener.
class PolkitAgent
: public QObject
, public QQmlParserStatus {
Q_OBJECT;
QML_ELEMENT;
Q_INTERFACES(QQmlParserStatus);
Q_DISABLE_COPY_MOVE(PolkitAgent);
/// The D-Bus path that this agent listener will use.
///
/// If not set, a default of /org/quickshell/Polkit will be used.
Q_PROPERTY(QString path READ path WRITE setPath);
/// Indicates whether the agent registered successfully and is in use.
Q_PROPERTY(bool isRegistered READ default NOTIFY isRegisteredChanged BINDABLE isRegistered);
/// Indicates an ongoing authentication request.
///
/// If this is true, other properties such as @@message and @@iconName will
/// also be populated with relevant information.
Q_PROPERTY(bool isActive READ default NOTIFY isActiveChanged BINDABLE isActive);
/// The current authentication state if an authentication request is active.
///
/// Null when no authentication request is active.
Q_PROPERTY(AuthFlow* flow READ default NOTIFY flowChanged BINDABLE flow);
public:
explicit PolkitAgent(QObject* parent = nullptr): QObject(parent) {};
~PolkitAgent() override;
void classBegin() override {};
void componentComplete() override;
[[nodiscard]] QString path() const { return this->mPath; };
void setPath(const QString& path);
[[nodiscard]] QBindable<AuthFlow*> flow() { return &this->bFlow; };
[[nodiscard]] QBindable<bool> isActive() { return &this->bIsActive; };
[[nodiscard]] QBindable<bool> isRegistered() { return &this->bIsRegistered; };
signals:
/// Emitted when an application makes a request that requires authentication.
///
/// At this point, @@state will be populated with relevant information.
/// Note that signals for conversation outcome are emitted from the @@AuthFlow instance.
void authenticationRequestStarted();
void isRegisteredChanged();
void isActiveChanged();
void flowChanged();
private:
QString mPath = "";
Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, AuthFlow*, bFlow, &PolkitAgent::flowChanged);
Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, bool, bIsActive, &PolkitAgent::isActiveChanged);
Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, bool, bIsRegistered, &PolkitAgent::isRegisteredChanged);
};
} // namespace qs::service::polkit

View file

@ -0,0 +1,68 @@
#include "session.hpp"
#include <glib-object.h>
#include <glib.h>
#include <qobject.h>
#include <qtmetamacros.h>
#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE
// This causes a problem with variables of the name.
#undef signals
#include <polkitagent/polkitagent.h>
#define signals Q_SIGNALS
namespace qs::service::polkit {
namespace {
void completedCb(PolkitAgentSession* /*session*/, gboolean gainedAuthorization, gpointer userData) {
auto* self = static_cast<Session*>(userData);
emit self->completed(gainedAuthorization);
}
void requestCb(
PolkitAgentSession* /*session*/,
const char* message,
gboolean echo,
gpointer userData
) {
auto* self = static_cast<Session*>(userData);
emit self->request(QString::fromUtf8(message), echo);
}
void showErrorCb(PolkitAgentSession* /*session*/, const char* message, gpointer userData) {
auto* self = static_cast<Session*>(userData);
emit self->showError(QString::fromUtf8(message));
}
void showInfoCb(PolkitAgentSession* /*session*/, const char* message, gpointer userData) {
auto* self = static_cast<Session*>(userData);
emit self->showInfo(QString::fromUtf8(message));
}
} // namespace
Session::Session(PolkitIdentity* identity, const QString& cookie, QObject* parent)
: QObject(parent) {
this->session = polkit_agent_session_new(identity, cookie.toUtf8().constData());
g_signal_connect(G_OBJECT(this->session), "completed", G_CALLBACK(completedCb), this);
g_signal_connect(G_OBJECT(this->session), "request", G_CALLBACK(requestCb), this);
g_signal_connect(G_OBJECT(this->session), "show-error", G_CALLBACK(showErrorCb), this);
g_signal_connect(G_OBJECT(this->session), "show-info", G_CALLBACK(showInfoCb), this);
}
Session::~Session() {
// Signals do not need to be disconnected explicitly. This happens during
// destruction of the gobject. Since we own the session object, we can be
// sure it is being destroyed after the unref.
g_object_unref(this->session);
}
void Session::initiate() { polkit_agent_session_initiate(this->session); }
void Session::cancel() { polkit_agent_session_cancel(this->session); }
void Session::respond(const QString& response) {
polkit_agent_session_response(this->session, response.toUtf8().constData());
}
} // namespace qs::service::polkit

View file

@ -0,0 +1,52 @@
#pragma once
#include <qobject.h>
// _PolkitIdentity and _PolkitAgentSession are considered reserved identifiers,
// but I am specifically forward declaring those reserved names.
// NOLINTBEGIN(bugprone-reserved-identifier)
using PolkitIdentity = struct _PolkitIdentity;
using PolkitAgentSession = struct _PolkitAgentSession;
// NOLINTEND(bugprone-reserved-identifier)
namespace qs::service::polkit {
//! Represents an authentication session for a specific identity.
class Session: public QObject {
Q_OBJECT;
Q_DISABLE_COPY_MOVE(Session);
public:
explicit Session(PolkitIdentity* identity, const QString& cookie, QObject* parent = nullptr);
~Session() override;
/// Call this after connecting to the relevant signals.
void initiate();
/// Call this to abort a running authentication session.
void cancel();
/// Provide a response to an input request.
void respond(const QString& response);
Q_SIGNALS:
/// Emitted when the session wants to request input from the user.
///
/// The message is a prompt to present to the user.
/// If echo is false, the user's response should not be displayed (e.g. for passwords).
void request(const QString& message, bool echo);
/// Emitted when the authentication session completes.
///
/// If success is true, authentication was successful.
/// Otherwise it failed (e.g. wrong password).
void completed(bool success);
/// Emitted when an error message should be shown to the user.
void showError(const QString& message);
/// Emitted when an informational message should be shown to the user.
void showInfo(const QString& message);
private:
PolkitAgentSession* session = nullptr;
};
} // namespace qs::service::polkit

View file

@ -0,0 +1,97 @@
import Quickshell
import Quickshell.Services.Polkit
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Scope {
id: root
FloatingWindow {
title: "Authentication Required"
visible: polkitAgent.isActive
color: contentItem.palette.window
ColumnLayout {
id: contentColumn
anchors.fill: parent
anchors.margins: 18
spacing: 12
Item { Layout.fillHeight: true }
Label {
Layout.fillWidth: true
text: polkitAgent.flow?.message || "<no message>"
wrapMode: Text.Wrap
font.bold: true
}
Label {
Layout.fillWidth: true
text: polkitAgent.flow?.supplementaryMessage || "<no supplementary message>"
wrapMode: Text.Wrap
opacity: 0.8
}
Label {
Layout.fillWidth: true
text: polkitAgent.flow?.inputPrompt || "<no input prompt>"
wrapMode: Text.Wrap
}
Label {
text: "Authentication failed, try again"
color: "red"
visible: polkitAgent.flow?.failed
}
TextField {
id: passwordInput
echoMode: polkitAgent.flow?.responseVisible
? TextInput.Normal : TextInput.Password
selectByMouse: true
Layout.fillWidth: true
onAccepted: okButton.clicked()
}
RowLayout {
spacing: 8
Button {
id: okButton
text: "OK"
enabled: passwordInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired
onClicked: {
polkitAgent.flow.submit(passwordInput.text)
passwordInput.text = ""
passwordInput.forceActiveFocus()
}
}
Button {
text: "Cancel"
visible: polkitAgent.isActive
onClicked: {
polkitAgent.flow.cancelAuthenticationRequest()
passwordInput.text = ""
}
}
}
Item { Layout.fillHeight: true }
}
Connections {
target: polkitAgent.flow
function onIsResponseRequiredChanged() {
passwordInput.text = ""
if (polkitAgent.flow.isResponseRequired)
passwordInput.forceActiveFocus()
}
}
}
PolkitAgent {
id: polkitAgent
}
}