service/polkit: add service module to write Polkit agents
This commit is contained in:
parent
1b147a2c78
commit
5ee8b62671
23 changed files with 1551 additions and 1 deletions
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
|
|
@ -53,6 +53,7 @@ jobs:
|
|||
libxcb \
|
||||
libpipewire \
|
||||
cli11 \
|
||||
polkit \
|
||||
jemalloc
|
||||
|
||||
- name: Build
|
||||
|
|
|
|||
7
BUILD.md
7
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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ set shell id.
|
|||
|
||||
## New Features
|
||||
|
||||
- Added support for creating Polkit agents.
|
||||
- Added support for creating wayland idle inhibitors.
|
||||
- Added support for wayland idle timeouts.
|
||||
- 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 escape sequence handling in desktop entries.
|
||||
|
||||
## Packaging Changes
|
||||
|
||||
`glib` and `polkit` have been added as dependencies when compiling with polkit agent support.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
];
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
libxcb
|
||||
libxkbcommon
|
||||
linux-pam
|
||||
polkit
|
||||
mesa
|
||||
pipewire
|
||||
qtbase
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
35
src/services/polkit/CMakeLists.txt
Normal file
35
src/services/polkit/CMakeLists.txt
Normal 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)
|
||||
179
src/services/polkit/agentimpl.cpp
Normal file
179
src/services/polkit/agentimpl.cpp
Normal 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
|
||||
66
src/services/polkit/agentimpl.hpp
Normal file
66
src/services/polkit/agentimpl.hpp
Normal 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
|
||||
163
src/services/polkit/flow.cpp
Normal file
163
src/services/polkit/flow.cpp
Normal 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
|
||||
179
src/services/polkit/flow.hpp
Normal file
179
src/services/polkit/flow.hpp
Normal 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
|
||||
65
src/services/polkit/gobjectref.hpp
Normal file
65
src/services/polkit/gobjectref.hpp
Normal 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
|
||||
84
src/services/polkit/identity.cpp
Normal file
84
src/services/polkit/identity.cpp
Normal 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
|
||||
64
src/services/polkit/identity.hpp
Normal file
64
src/services/polkit/identity.hpp
Normal 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
|
||||
234
src/services/polkit/listener.cpp
Normal file
234
src/services/polkit/listener.cpp
Normal 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)
|
||||
75
src/services/polkit/listener.hpp
Normal file
75
src/services/polkit/listener.hpp
Normal 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
|
||||
46
src/services/polkit/module.md
Normal file
46
src/services/polkit/module.md
Normal 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.
|
||||
35
src/services/polkit/qml.cpp
Normal file
35
src/services/polkit/qml.cpp
Normal 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
|
||||
84
src/services/polkit/qml.hpp
Normal file
84
src/services/polkit/qml.hpp
Normal 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
|
||||
68
src/services/polkit/session.cpp
Normal file
68
src/services/polkit/session.cpp
Normal 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
|
||||
52
src/services/polkit/session.hpp
Normal file
52
src/services/polkit/session.hpp
Normal 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
|
||||
97
src/services/polkit/test/manual/agent.qml
Normal file
97
src/services/polkit/test/manual/agent.qml
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue