From e87903deda060c57968241b979b222f3ab57382a Mon Sep 17 00:00:00 2001 From: Cu3PO42 Date: Thu, 9 Oct 2025 23:50:08 +0200 Subject: [PATCH] service/polkit: add service module to write PolKit agents --- .github/workflows/build.yml | 1 + BUILD.md | 7 + CMakeLists.txt | 1 + default.nix | 7 +- quickshell.scm | 1 + src/services/CMakeLists.txt | 4 + src/services/polkit/CMakeLists.txt | 35 ++++ src/services/polkit/agentimpl.cpp | 183 +++++++++++++++++ src/services/polkit/agentimpl.hpp | 74 +++++++ src/services/polkit/flow.cpp | 166 +++++++++++++++ src/services/polkit/flow.hpp | 193 ++++++++++++++++++ src/services/polkit/gobjectref.hpp | 65 ++++++ src/services/polkit/identity.cpp | 93 +++++++++ src/services/polkit/identity.hpp | 64 ++++++ src/services/polkit/listener.cpp | 234 ++++++++++++++++++++++ src/services/polkit/listener.hpp | 75 +++++++ src/services/polkit/module.md | 46 +++++ src/services/polkit/qml.cpp | 49 +++++ src/services/polkit/qml.hpp | 89 ++++++++ src/services/polkit/session.cpp | 68 +++++++ src/services/polkit/session.hpp | 52 +++++ src/services/polkit/test/manual/agent.qml | 84 ++++++++ 22 files changed, 1590 insertions(+), 1 deletion(-) create mode 100644 src/services/polkit/CMakeLists.txt create mode 100644 src/services/polkit/agentimpl.cpp create mode 100644 src/services/polkit/agentimpl.hpp create mode 100644 src/services/polkit/flow.cpp create mode 100644 src/services/polkit/flow.hpp create mode 100644 src/services/polkit/gobjectref.hpp create mode 100644 src/services/polkit/identity.cpp create mode 100644 src/services/polkit/identity.hpp create mode 100644 src/services/polkit/listener.cpp create mode 100644 src/services/polkit/listener.hpp create mode 100644 src/services/polkit/module.md create mode 100644 src/services/polkit/qml.cpp create mode 100644 src/services/polkit/qml.hpp create mode 100644 src/services/polkit/session.cpp create mode 100644 src/services/polkit/session.hpp create mode 100644 src/services/polkit/test/manual/agent.qml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83957dc2..9a3d0972 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,7 @@ jobs: libxcb \ libpipewire \ cli11 \ + polkit \ jemalloc - name: Build diff --git a/BUILD.md b/BUILD.md index 742baa7b..fdea27e2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -192,6 +192,13 @@ To disable: `-DSERVICE_PAM=OFF` Dependencies: `pam` +### Polkit +This feature enables creating Polkit agents that can prompt user for authentication. + +To disable: `-DSERVICE_POLKIT=OFF` + +Dependencies: `polkit`, `glib` + ### Hyprland This feature enables hyprland specific integrations. It requires wayland support but has no extra dependencies. diff --git a/CMakeLists.txt b/CMakeLists.txt index 880b9ca3..c8670013 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON) boption(SERVICE_MPRIS "Mpris" ON) boption(SERVICE_PAM "Pam" ON) +boption(SERVICE_POLKIT "Polkit" ON) boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) diff --git a/default.nix b/default.nix index adb978b7..a00f0f13 100644 --- a/default.nix +++ b/default.nix @@ -21,6 +21,8 @@ libgbm ? null, pipewire, pam, + polkit, + glib, gitRev ? (let headExists = builtins.pathExists ./.git/HEAD; @@ -43,6 +45,7 @@ withPam ? true, withHyprland ? true, withI3 ? true, + withPolkit ? true, }: let unwrapped = stdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; @@ -76,7 +79,8 @@ ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] ++ lib.optional withX11 xorg.libxcb ++ lib.optional withPam pam - ++ lib.optional withPipewire pipewire; + ++ lib.optional withPipewire pipewire + ++ lib.optionals withPolkit [ polkit glib ]; cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; @@ -91,6 +95,7 @@ (lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PAM" withPam) + (lib.cmakeBool "SERVICE_POLKIT" withPolkit) (lib.cmakeBool "HYPRLAND" withHyprland) (lib.cmakeBool "I3" withI3) ]; diff --git a/quickshell.scm b/quickshell.scm index 26abdc0b..3f821605 100644 --- a/quickshell.scm +++ b/quickshell.scm @@ -42,6 +42,7 @@ libxcb libxkbcommon linux-pam + polkit mesa pipewire qtbase diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 5ab5c550..f3912a99 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -14,6 +14,10 @@ if (SERVICE_PAM) add_subdirectory(pam) endif() +if (SERVICE_POLKIT) + add_subdirectory(polkit) +endif() + if (SERVICE_GREETD) add_subdirectory(greetd) endif() diff --git a/src/services/polkit/CMakeLists.txt b/src/services/polkit/CMakeLists.txt new file mode 100644 index 00000000..51791d8a --- /dev/null +++ b/src/services/polkit/CMakeLists.txt @@ -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) diff --git a/src/services/polkit/agentimpl.cpp b/src/services/polkit/agentimpl.cpp new file mode 100644 index 00000000..7d396af0 --- /dev/null +++ b/src/services/polkit/agentimpl.cpp @@ -0,0 +1,183 @@ +#include "agentimpl.hpp" +#include +#include + +#include +#include +#include +#include + +#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"); +} + +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::tryTakeover(PolkitAgent* agent) { + if (instance == nullptr) return nullptr; + + 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; + + return instance; +} + +void PolkitAgentImpl::onEndOfQmlAgent(PolkitAgent* agent) { + if (instance != nullptr && instance->qmlAgent == agent) { + delete instance; + instance = nullptr; + } +} + +QBindable PolkitAgentImpl::activeFlow() { return &this->bActiveFlow; } +QBindable PolkitAgentImpl::isRegistered() { return &this->bIsRegistered; } + +const QString& PolkitAgentImpl::getPath() const { return this->path; } + +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 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->isActiveChanged(); + emit this->qmlAgent->flowChanged(); + 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 diff --git a/src/services/polkit/agentimpl.hpp b/src/services/polkit/agentimpl.hpp new file mode 100644 index 00000000..63080e98 --- /dev/null +++ b/src/services/polkit/agentimpl.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include +#include + +#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* tryTakeover(PolkitAgent* agent); + static void onEndOfQmlAgent(PolkitAgent* agent); + + [[nodiscard]] QBindable activeFlow(); + [[nodiscard]] QBindable isRegistered(); + + [[nodiscard]] const QString& getPath() const; + 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 listener; + PolkitAgent* qmlAgent = nullptr; + QString path; + + std::deque queuedRequests; + + Q_OBJECT_BINDABLE_PROPERTY( + PolkitAgentImpl, + AuthFlow*, + bActiveFlow, + &PolkitAgentImpl::activeFlowChanged + ); + Q_OBJECT_BINDABLE_PROPERTY( + PolkitAgentImpl, + bool, + bIsRegistered, + &PolkitAgentImpl::isRegisteredChanged + ); +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/flow.cpp b/src/services/polkit/flow.cpp new file mode 100644 index 00000000..cc7f67c9 --- /dev/null +++ b/src/services/polkit/flow.cpp @@ -0,0 +1,166 @@ +#include "flow.hpp" +#include + +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "identity.hpp" +#include "qml.hpp" +#include "session.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkitState, "quickshell.service.polkit.state"); +} + +namespace qs::service::polkit { +AuthFlow::AuthFlow(AuthRequest* request, QList&& 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; }; + +const QString& AuthFlow::message() const { return this->mRequest->message; } +const QString& AuthFlow::iconName() const { return this->mRequest->iconName; } +const QString& AuthFlow::actionId() const { return this->mRequest->actionId; } +const QString& AuthFlow::cookie() const { return this->mRequest->cookie; } +const QList& AuthFlow::identities() const { return this->mIdentities; } + +QBindable AuthFlow::selectedIdentity() { return &this->bSelectedIdentity; } + +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(); +} + +QBindable AuthFlow::isResponseRequired() { return &this->bIsResponseRequired; } +QBindable AuthFlow::inputPrompt() { return &this->bInputPrompt; } +QBindable AuthFlow::responseVisible() { return &this->bResponseVisible; } +QBindable AuthFlow::supplementaryMessage() { return &this->bSupplementaryMessage; } +QBindable AuthFlow::supplementaryIsError() { return &this->bSupplementaryIsError; } +QBindable AuthFlow::isCompleted() { return &this->bIsCompleted; } +QBindable AuthFlow::isSuccessful() { return &this->bIsSuccessful; } +QBindable AuthFlow::isCancelled() { return &this->bIsCancelled; } +AuthRequest* AuthFlow::authRequest() const { return this->mRequest; } + +void AuthFlow::cancelFromAgent() { + if (!this->currentSession) return; + + qCDebug(logPolkitState) << "cancelling authentication request from agent"; + + this->bIsCancelled = true; + this->currentSession->cancel(); + + 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); + + this->bIsResponseRequired = false; + this->bInputPrompt = QString {}; + this->bResponseVisible = false; +} + +void AuthFlow::cancelAuthenticationRequest() { + if (!this->currentSession) return; + + qCDebug(logPolkitState) << "cancelling authentication request by user request"; + + this->bIsCancelled = true; + this->currentSession->cancel(); + + 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::clearState() { + this->bIsResponseRequired = false; + this->bInputPrompt = QString {}; + this->bResponseVisible = false; + this->bSupplementaryMessage = QString {}; + this->bSupplementaryIsError = false; +} + +void AuthFlow::request(const QString& message, bool echo) { + this->bIsResponseRequired = true; + this->bInputPrompt = message; + this->bResponseVisible = echo; +} + +void AuthFlow::completed(bool gainedAuthorization) { + qCDebug(logPolkitState) << "authentication session completed, gainedAuthorization =" + << gainedAuthorization << ", isCancelled =" << this->bIsCancelled.value(); + + if (gainedAuthorization) { + this->bIsCompleted = true; + this->bIsSuccessful = true; + this->mRequest->complete(); + + emit this->authenticationSucceeded(); + } else if (this->bIsCancelled.value()) { + this->bIsCompleted = true; + this->bIsSuccessful = false; + } else { + emit this->authenticationFailed(); + + this->clearState(); + this->setupSession(); + } +} + +void AuthFlow::showError(const QString& message) { + this->bSupplementaryMessage = message; + this->bSupplementaryIsError = true; +} + +void AuthFlow::showInfo(const QString& message) { + this->bSupplementaryMessage = message; + this->bSupplementaryIsError = false; +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/flow.hpp b/src/services/polkit/flow.hpp new file mode 100644 index 00000000..54feda1b --- /dev/null +++ b/src/services/polkit/flow.hpp @@ -0,0 +1,193 @@ +#pragma once + +#include +#include +#include + +#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 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); + // clang-format on + +public: + explicit AuthFlow(AuthRequest* request, QList&& 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; + [[nodiscard]] const QString& iconName() const; + [[nodiscard]] const QString& actionId() const; + [[nodiscard]] const QString& cookie() const; + [[nodiscard]] const QList& identities() const; + + [[nodiscard]] QBindable selectedIdentity(); + void setSelectedIdentity(Identity* identity); + + [[nodiscard]] QBindable isResponseRequired(); + [[nodiscard]] QBindable inputPrompt(); + [[nodiscard]] QBindable responseVisible(); + + [[nodiscard]] QBindable supplementaryMessage(); + [[nodiscard]] QBindable supplementaryIsError(); + + [[nodiscard]] QBindable isCompleted(); + [[nodiscard]] QBindable isSuccessful(); + [[nodiscard]] QBindable isCancelled(); + + [[nodiscard]] AuthRequest* authRequest() const; + +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(); + +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(); + /// Clear all state variables. + void clearState(); + + Session* currentSession = nullptr; + AuthRequest* mRequest = nullptr; + QList mIdentities; + + 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); +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/gobjectref.hpp b/src/services/polkit/gobjectref.hpp new file mode 100644 index 00000000..cd29a9d6 --- /dev/null +++ b/src/services/polkit/gobjectref.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +namespace qs::service::polkit { + +struct GObjectNoRefTag {}; +constexpr GObjectNoRefTag G_OBJECT_NO_REF; + +template +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& other) const { return this->ptr == other.ptr; } + +private: + T* ptr; +}; +} // namespace qs::service::polkit \ No newline at end of file diff --git a/src/services/polkit/identity.cpp b/src/services/polkit/identity.cpp new file mode 100644 index 00000000..39670a25 --- /dev/null +++ b/src/services/polkit/identity.cpp @@ -0,0 +1,93 @@ +#include "identity.hpp" +#include +#include +#include + +#include +#include +#include +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// Workaround macro collision with glib 'signals' struct member. +#undef signals +#include +#define signals Q_SIGNALS +#include +#include +#include + +#include "gobjectref.hpp" + +namespace qs::service::polkit { +Identity::Identity( + id_t id, + QString name, + QString displayName, + bool isGroup, + GObjectRef polkitIdentity, + QObject* parent +) + : QObject(parent) + , polkitIdentity(std::move(polkitIdentity)) + , mId(id) + , mName(std::move(name)) + , mDisplayName(std::move(displayName)) + , mIsGroup(isGroup) {} + +Identity::~Identity() = default; + +Identity* Identity::fromPolkitIdentity(GObjectRef 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(bufSize); + + std::aligned_storage_t pwBuf; + passwd* pw = nullptr; + getpwuid_r(uid, reinterpret_cast(&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(bufSize); + + std::aligned_storage_t grBuf; + group* gr = nullptr; + getgrgid_r(gid, reinterpret_cast(&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; +} + +quint32 Identity::id() const { return this->mId; } +const QString& Identity::name() const { return this->mName; } +const QString& Identity::displayName() const { return this->mDisplayName; } +bool Identity::isGroup() const { return this->mIsGroup; } + +} // namespace qs::service::polkit diff --git a/src/services/polkit/identity.hpp b/src/services/polkit/identity.hpp new file mode 100644 index 00000000..32c766d7 --- /dev/null +++ b/src/services/polkit/identity.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +#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, + QObject* parent = nullptr + ); + ~Identity() override; + + static Identity* fromPolkitIdentity(GObjectRef identity); + + [[nodiscard]] quint32 id() const; + [[nodiscard]] const QString& name() const; + [[nodiscard]] const QString& displayName() const; + [[nodiscard]] bool isGroup() const; + + GObjectRef polkitIdentity; + +private: + id_t mId; + QString mName; + QString mDisplayName; + bool mIsGroup; +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/listener.cpp b/src/services/polkit/listener.cpp new file mode 100644 index 00000000..643292c4 --- /dev/null +++ b/src/services/polkit/listener.cpp @@ -0,0 +1,234 @@ +#include "listener.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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 data(reinterpret_cast(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 : ""); + 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(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(self), nullptr, callback, userData); + + // Identities may be duplicated, so we use the hash to filter them out. + std::unordered_set identitySet; + std::vector> identityVector; + for (auto* item = g_list_first(identities); item != nullptr; item = g_list_next(item)) { + auto* identity = static_cast(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(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) \ No newline at end of file diff --git a/src/services/polkit/listener.hpp b/src/services/polkit/listener.hpp new file mode 100644 index 00000000..996fa23b --- /dev/null +++ b/src/services/polkit/listener.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// This causes a problem with variables of the name. +#undef signals + +#include +#include + +#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> 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 diff --git a/src/services/polkit/module.md b/src/services/polkit/module.md new file mode 100644 index 00000000..ae46acf1 --- /dev/null +++ b/src/services/polkit/module.md @@ -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. diff --git a/src/services/polkit/qml.cpp b/src/services/polkit/qml.cpp new file mode 100644 index 00000000..b9581982 --- /dev/null +++ b/src/services/polkit/qml.cpp @@ -0,0 +1,49 @@ +#include "qml.hpp" + +#include +#include + +#include "../../core/logcat.hpp" +#include "agentimpl.hpp" +#include "flow.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkit, "quickshell.service.polkit"); +} + +namespace qs::service::polkit { +PolkitAgent::PolkitAgent(QObject* parent): QObject(parent) { + // Try to takeover an existing PolkitAgentImpl from the previous QML engine generation. + // If none exists, we wait until componentComplete to create one so we have the correct path. + PolkitAgentImpl::tryTakeover(this); +} + +PolkitAgent::~PolkitAgent() { PolkitAgentImpl::onEndOfQmlAgent(this); }; + +void PolkitAgent::componentComplete() { + if (this->mPath.isEmpty()) this->mPath = "/org/quickshell/Polkit"; + + auto* impl = PolkitAgentImpl::tryGetOrCreate(this); + if (impl == nullptr) return; + + impl->setPath(this->mPath); + + this->bFlow.setBinding([impl]() -> AuthFlow* { return impl->activeFlow().value(); }); + this->bIsActive.setBinding([impl]() -> bool { return impl->activeFlow().value() != nullptr; }); + this->bIsRegistered.setBinding([impl]() -> bool { return impl->isRegistered().value(); }); +} + +QString PolkitAgent::path() const { return this->mPath; } + +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."; + } +} + +QBindable PolkitAgent::flow() { return &this->bFlow; } +QBindable PolkitAgent::isActive() { return &this->bIsActive; } +QBindable PolkitAgent::isRegistered() { return &this->bIsRegistered; } +} // namespace qs::service::polkit diff --git a/src/services/polkit/qml.hpp b/src/services/polkit/qml.hpp new file mode 100644 index 00000000..b08f42b4 --- /dev/null +++ b/src/services/polkit/qml.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "../../core/doc.hpp" +#include "../../core/model.hpp" +#include "../../core/reload.hpp" +#include "../../core/retainable.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); + + // clang-format off + /// 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); + // clang-format on + +public: + explicit PolkitAgent(QObject* parent = nullptr); + ~PolkitAgent() override; + + void classBegin() override {}; + void componentComplete() override; + + [[nodiscard]] QString path() const; + void setPath(const QString& path); + + [[nodiscard]] QBindable flow(); + [[nodiscard]] QBindable isActive(); + [[nodiscard]] QBindable isRegistered(); + +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 diff --git a/src/services/polkit/session.cpp b/src/services/polkit/session.cpp new file mode 100644 index 00000000..71def68a --- /dev/null +++ b/src/services/polkit/session.cpp @@ -0,0 +1,68 @@ +#include "session.hpp" + +#include +#include +#include +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// This causes a problem with variables of the name. +#undef signals +#include +#define signals Q_SIGNALS + +namespace qs::service::polkit { + +namespace { +void completedCb(PolkitAgentSession* /*session*/, gboolean gainedAuthorization, gpointer userData) { + auto* self = static_cast(userData); + emit self->completed(gainedAuthorization); +} + +void requestCb( + PolkitAgentSession* /*session*/, + const char* message, + gboolean echo, + gpointer userData +) { + auto* self = static_cast(userData); + emit self->request(QString::fromUtf8(message), echo); +} + +void showErrorCb(PolkitAgentSession* /*session*/, const char* message, gpointer userData) { + auto* self = static_cast(userData); + emit self->showError(QString::fromUtf8(message)); +} + +void showInfoCb(PolkitAgentSession* /*session*/, const char* message, gpointer userData) { + auto* self = static_cast(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 diff --git a/src/services/polkit/session.hpp b/src/services/polkit/session.hpp new file mode 100644 index 00000000..29331b1f --- /dev/null +++ b/src/services/polkit/session.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +// _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 diff --git a/src/services/polkit/test/manual/agent.qml b/src/services/polkit/test/manual/agent.qml new file mode 100644 index 00000000..557ae446 --- /dev/null +++ b/src/services/polkit/test/manual/agent.qml @@ -0,0 +1,84 @@ +import Quickshell +import Quickshell.Services.Polkit +import QtQuick +import QtQuick.Controls + +Scope { + id: root + + FloatingWindow { + title: "Authentication Required" + + visible: polkitAgent.isActive + + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: 18 + spacing: 12 + + Label { + text: polkitAgent.flow?.message ?? "" + wrapMode: Text.Wrap + font.bold: true + } + + Label { + visible: polkitAgent.flow?.supplementaryMessage.length > 0 + text: polkitAgent.flow?.supplementaryMessage ?? "" + wrapMode: Text.Wrap + opacity: 0.8 + } + + Label { + text: polkitAgent.flow?.inputPrompt ?? "" + wrapMode: Text.Wrap + } + + TextInput { + id: passwordInput + echoMode: polkitAgent.flow?.responseVisible + ? TextInput.Normal : TextInput.Password + selectByMouse: true + width: parent.width + onAccepted: okButton.clicked() + } + + Row { + 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 = "" + } + } + } + } + + Connections { + target: polkitAgent.flow + function onIsResponseRequiredChanged() { + passwordInput.text = "" + if (polkitAgent.flow.isResponseRequired) + passwordInput.forceActiveFocus() + } + } + } + + PolkitAgent { + id: polkitAgent + path: "/org/quickshell/PolkitAgent" + } +}