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 \
|
libxcb \
|
||||||
libpipewire \
|
libpipewire \
|
||||||
cli11 \
|
cli11 \
|
||||||
|
polkit \
|
||||||
jemalloc
|
jemalloc
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
|
||||||
7
BUILD.md
7
BUILD.md
|
|
@ -192,6 +192,13 @@ To disable: `-DSERVICE_PAM=OFF`
|
||||||
|
|
||||||
Dependencies: `pam`
|
Dependencies: `pam`
|
||||||
|
|
||||||
|
### Polkit
|
||||||
|
This feature enables creating Polkit agents that can prompt user for authentication.
|
||||||
|
|
||||||
|
To disable: `-DSERVICE_POLKIT=OFF`
|
||||||
|
|
||||||
|
Dependencies: `polkit`, `glib`
|
||||||
|
|
||||||
### Hyprland
|
### Hyprland
|
||||||
This feature enables hyprland specific integrations. It requires wayland support
|
This feature enables hyprland specific integrations. It requires wayland support
|
||||||
but has no extra dependencies.
|
but has no extra dependencies.
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
|
||||||
boption(SERVICE_PIPEWIRE "PipeWire" ON)
|
boption(SERVICE_PIPEWIRE "PipeWire" ON)
|
||||||
boption(SERVICE_MPRIS "Mpris" ON)
|
boption(SERVICE_MPRIS "Mpris" ON)
|
||||||
boption(SERVICE_PAM "Pam" ON)
|
boption(SERVICE_PAM "Pam" ON)
|
||||||
|
boption(SERVICE_POLKIT "Polkit" ON)
|
||||||
boption(SERVICE_GREETD "Greetd" ON)
|
boption(SERVICE_GREETD "Greetd" ON)
|
||||||
boption(SERVICE_UPOWER "UPower" ON)
|
boption(SERVICE_UPOWER "UPower" ON)
|
||||||
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
|
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ set shell id.
|
||||||
|
|
||||||
## New Features
|
## New Features
|
||||||
|
|
||||||
|
- Added support for creating Polkit agents.
|
||||||
- Added support for creating wayland idle inhibitors.
|
- Added support for creating wayland idle inhibitors.
|
||||||
- Added support for wayland idle timeouts.
|
- Added support for wayland idle timeouts.
|
||||||
- Added the ability to override Quickshell.cacheDir with a custom path.
|
- Added the ability to override Quickshell.cacheDir with a custom path.
|
||||||
|
|
@ -23,3 +24,7 @@ set shell id.
|
||||||
|
|
||||||
- Fixed volume control breaking with pipewire pro audio mode.
|
- Fixed volume control breaking with pipewire pro audio mode.
|
||||||
- Fixed escape sequence handling in desktop entries.
|
- Fixed escape sequence handling in desktop entries.
|
||||||
|
|
||||||
|
## Packaging Changes
|
||||||
|
|
||||||
|
`glib` and `polkit` have been added as dependencies when compiling with polkit agent support.
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@
|
||||||
libgbm ? null,
|
libgbm ? null,
|
||||||
pipewire,
|
pipewire,
|
||||||
pam,
|
pam,
|
||||||
|
polkit,
|
||||||
|
glib,
|
||||||
|
|
||||||
gitRev ? (let
|
gitRev ? (let
|
||||||
headExists = builtins.pathExists ./.git/HEAD;
|
headExists = builtins.pathExists ./.git/HEAD;
|
||||||
|
|
@ -43,6 +45,7 @@
|
||||||
withPam ? true,
|
withPam ? true,
|
||||||
withHyprland ? true,
|
withHyprland ? true,
|
||||||
withI3 ? true,
|
withI3 ? true,
|
||||||
|
withPolkit ? true,
|
||||||
}: let
|
}: let
|
||||||
unwrapped = stdenv.mkDerivation {
|
unwrapped = stdenv.mkDerivation {
|
||||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||||
|
|
@ -76,7 +79,8 @@
|
||||||
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
|
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
|
||||||
++ lib.optional withX11 xorg.libxcb
|
++ lib.optional withX11 xorg.libxcb
|
||||||
++ lib.optional withPam pam
|
++ lib.optional withPam pam
|
||||||
++ lib.optional withPipewire pipewire;
|
++ lib.optional withPipewire pipewire
|
||||||
|
++ lib.optionals withPolkit [ polkit glib ];
|
||||||
|
|
||||||
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
|
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
|
||||||
|
|
||||||
|
|
@ -91,6 +95,7 @@
|
||||||
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
|
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
|
||||||
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
|
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
|
||||||
(lib.cmakeBool "SERVICE_PAM" withPam)
|
(lib.cmakeBool "SERVICE_PAM" withPam)
|
||||||
|
(lib.cmakeBool "SERVICE_POLKIT" withPolkit)
|
||||||
(lib.cmakeBool "HYPRLAND" withHyprland)
|
(lib.cmakeBool "HYPRLAND" withHyprland)
|
||||||
(lib.cmakeBool "I3" withI3)
|
(lib.cmakeBool "I3" withI3)
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
libxcb
|
libxcb
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
linux-pam
|
linux-pam
|
||||||
|
polkit
|
||||||
mesa
|
mesa
|
||||||
pipewire
|
pipewire
|
||||||
qtbase
|
qtbase
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ if (SERVICE_PAM)
|
||||||
add_subdirectory(pam)
|
add_subdirectory(pam)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (SERVICE_POLKIT)
|
||||||
|
add_subdirectory(polkit)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (SERVICE_GREETD)
|
if (SERVICE_GREETD)
|
||||||
add_subdirectory(greetd)
|
add_subdirectory(greetd)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
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