Fix update discovery and isolate fingerprint installer permissions

This commit is contained in:
ajp_anton
2026-09-12 05:51:06 +00:00
parent 7f56368674
commit 61ef199667
14 changed files with 106 additions and 51 deletions
+2
View File
@@ -17,6 +17,8 @@ include(KDECompilerSettings NO_POLICY_SCOPE)
find_package(Qt6 6.8 REQUIRED COMPONENTS Core Quick)
find_package(KF6 6.0 REQUIRED COMPONENTS Auth Config CoreAddons I18n KCMUtils)
find_package(PkgConfig REQUIRED)
pkg_check_modules(RPM REQUIRED IMPORTED_TARGET rpm)
add_subdirectory(src)
if(BUILD_TESTING)
+5
View File
@@ -14,6 +14,11 @@ installation to a narrowly scoped KAuth helper. The helper accepts only valid
package names that independently resolve to that capability in the
`fedora-tools` repository.
Update detection compares repository and installed versions using RPM's version
ordering, without requiring access to DNF's privileged transaction state. DNF
still checks dependencies when installing an update. The repository file sets a
five-minute metadata expiry so Discover's PackageKit backend checks it regularly.
Installed tools can also be configured or removed from the module. Package
operations run one at a time; other actions remain disabled until DNF finishes.
The module currently configures task-group shortcut behaviour, touchpad hold-tap
@@ -1,6 +1,6 @@
Name: fedora-tools-settings
Version: 0.1.0
Release: 13%{?dist}
Release: 14%{?dist}
Summary: Plasma System Settings module for Fedora Tools
License: MIT
@@ -18,6 +18,7 @@ BuildRequires: kf6-kconfig-devel
BuildRequires: kf6-ki18n-devel
BuildRequires: kf6-kirigami-devel
BuildRequires: qt6-qtdeclarative-devel
BuildRequires: rpm-devel
Requires: dnf5
Requires: kf6-kauth
@@ -74,6 +75,10 @@ done
%{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy
%changelog
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-14
- Detect newer packages without reading privileged DNF state
- Report package query failures instead of silently hiding updates
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-13
- Share settings validation and report failed configuration writes
- Simplify ordering and source packaging
+1
View File
@@ -10,6 +10,7 @@ kcmutils_add_qml_kcm(kcm_fedora_tools
toolmodel.h
)
target_link_libraries(kcm_fedora_tools PRIVATE
PkgConfig::RPM
Qt6::Core
Qt6::Quick
KF6::AuthCore
+7 -35
View File
@@ -142,7 +142,6 @@ void FedoraToolsKcm::refresh(bool refreshMetadata)
m_refreshMetadata = refreshMetadata;
m_installedPackages.clear();
m_availablePackages.clear();
m_updates.clear();
setMessage({});
setBusy(true);
@@ -316,8 +315,12 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
const QByteArray output = m_query.readAllStandardOutput();
const QByteArray errorOutput = m_query.readAllStandardError();
if (exitStatus != QProcess::NormalExit) {
setMessage(i18n("Package discovery terminated unexpectedly."), true);
// rpm -q reports missing packages on stdout with exit code 1. That is a
// normal discovery result; stderr or a failed repository query is not.
const QString details = QString::fromUtf8(errorOutput).trimmed();
if (exitStatus != QProcess::NormalExit
|| (exitCode != 0 && (m_stage == QueryStage::Available || !details.isEmpty() || exitCode != 1))) {
setMessage(details.isEmpty() ? i18n("Package discovery failed (%1).", m_query.program()) : details, true);
m_stage = QueryStage::Idle;
setPackageOperation({}, {});
setBusy(false);
@@ -342,43 +345,12 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
}
if (m_stage == QueryStage::Available) {
if (exitCode != 0) {
m_tools.setPackages(m_installedPackages, {});
const QString details = QString::fromUtf8(errorOutput).trimmed();
setMessage(details.isEmpty() ? i18n("The Fedora Tools repository is unavailable.") : details, true);
updateFingerprintStatus();
m_stage = QueryStage::Idle;
setPackageOperation({}, {});
setBusy(false);
return;
}
m_availablePackages = parsePackageRecords(output);
if (m_availablePackages.isEmpty()) {
finishRefresh();
return;
}
startQuery(QStringLiteral("/usr/bin/dnf5"),
{QStringLiteral("--repo=fedora-tools"),
QStringLiteral("-q"),
QStringLiteral("repoquery"),
QStringLiteral("--available"),
QStringLiteral("--upgrades"),
QStringLiteral("--whatprovides=fedora-tools-tool"),
QStringLiteral("--queryformat"),
packageFormat},
QueryStage::Updates);
return;
}
if (m_stage == QueryStage::Updates) {
if (exitCode == 0) {
for (const PackageRecord &package : parsePackageRecords(output)) {
m_updates.insert(package.name);
}
}
QStringList packageNames;
for (const PackageRecord &package : std::as_const(m_availablePackages)) {
packageNames.append(package.name);
@@ -407,7 +379,7 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
void FedoraToolsKcm::finishRefresh()
{
m_tools.setPackages(m_installedPackages, m_availablePackages, m_updates);
m_tools.setPackages(m_installedPackages, m_availablePackages);
updateFingerprintStatus();
m_stage = QueryStage::Idle;
setPackageOperation({}, {});
@@ -8,7 +8,6 @@
#include <KQuickConfigModule>
#include <QProcess>
#include <QSet>
#include <QUrl>
class KJob;
@@ -75,7 +74,6 @@ private:
Idle,
InstalledProviders,
Available,
Updates,
KnownInstalled,
};
@@ -95,7 +93,6 @@ private:
QueryStage m_stage = QueryStage::Idle;
QList<PackageRecord> m_installedPackages;
QList<PackageRecord> m_availablePackages;
QSet<QString> m_updates;
bool m_refreshMetadata = false;
bool m_busy = false;
bool m_error = false;
+17 -3
View File
@@ -10,12 +10,26 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardPaths>
#include <rpm/rpmver.h>
#include <algorithm>
#include <utility>
namespace
{
bool newerVersion(const QString &available, const QString &installed)
{
const auto candidate = rpmverParse(available.toUtf8().constData());
const auto current = rpmverParse(installed.toUtf8().constData());
if (!candidate || !current) {
qWarning() << "Cannot compare RPM versions:" << available << installed;
}
const bool newer = candidate && current && rpmverCmp(candidate, current) > 0;
rpmverFree(candidate);
rpmverFree(current);
return newer;
}
QStringList readConfigurationCommand(const QString &packageName)
{
const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
@@ -119,8 +133,7 @@ QHash<int, QByteArray> ToolModel::roleNames() const
}
void ToolModel::setPackages(const QList<PackageRecord> &installed,
const QList<PackageRecord> &available,
const QSet<QString> &updates)
const QList<PackageRecord> &available)
{
QMap<QString, Tool> tools;
@@ -147,7 +160,8 @@ void ToolModel::setPackages(const QList<PackageRecord> &installed,
}
tool.compatibilityError = incompatible;
tool.available = incompatible.isEmpty();
tool.updateAvailable = tool.available && tool.installed && updates.contains(package.name);
tool.updateAvailable = tool.available && tool.installed
&& newerVersion(package.version, tool.version);
}
beginResetModel();
+1 -3
View File
@@ -5,7 +5,6 @@
#include "packageutils.h"
#include <QAbstractListModel>
#include <QSet>
#include <QStringList>
struct Tool
@@ -48,8 +47,7 @@ public:
QHash<int, QByteArray> roleNames() const override;
void setPackages(const QList<PackageRecord> &installed,
const QList<PackageRecord> &available,
const QSet<QString> &updates = {});
const QList<PackageRecord> &available);
bool mayInstall(const QString &packageName) const;
bool mayRemove(const QString &packageName) const;
QStringList configurationCommand(const QString &packageName) const;
+1 -1
View File
@@ -14,7 +14,7 @@ add_executable(test-toolmodel
../src/packageutils.cpp
)
target_include_directories(test-toolmodel PRIVATE ../src)
target_link_libraries(test-toolmodel PRIVATE Qt6::Core Qt6::Test)
target_link_libraries(test-toolmodel PRIVATE Qt6::Core Qt6::Test PkgConfig::RPM)
add_test(NAME toolmodel COMMAND test-toolmodel)
add_executable(test-qmlresource test-qmlresource.cpp)
+34 -2
View File
@@ -12,6 +12,38 @@ class ToolModelTest : public QObject
Q_OBJECT
private Q_SLOTS:
void comparesRpmVersions_data()
{
QTest::addColumn<QString>("installed");
QTest::addColumn<QString>("available");
QTest::addColumn<bool>("update");
QTest::newRow("numeric release") << QStringLiteral("0.1.0-9.fc44") << QStringLiteral("0.1.0-11.fc44") << true;
QTest::newRow("same version") << QStringLiteral("0.1.0-11.fc44") << QStringLiteral("0.1.0-11.fc44") << false;
QTest::newRow("locally newer") << QStringLiteral("0.1.0-25.fc44") << QStringLiteral("0.1.0-23.fc44") << false;
QTest::newRow("epoch wins") << QStringLiteral("2.0-1") << QStringLiteral("1:1.0-1") << true;
QTest::newRow("installed epoch wins") << QStringLiteral("1:1.0-1") << QStringLiteral("2.0-1") << false;
QTest::newRow("implicit zero epoch") << QStringLiteral("0:1.0-1") << QStringLiteral("1.0-1") << false;
QTest::newRow("prerelease") << QStringLiteral("1.0~rc1-1") << QStringLiteral("1.0-1") << true;
QTest::newRow("snapshot") << QStringLiteral("1.0-1") << QStringLiteral("1.0^git1-1") << true;
QTest::newRow("newer than snapshot") << QStringLiteral("1.0^git1-1") << QStringLiteral("1.0.1-1") << true;
}
void comparesRpmVersions()
{
QFETCH(QString, installed);
QFETCH(QString, available);
QFETCH(bool, update);
ToolModel model;
// Match an older noarch install against its native-architecture update.
// No DNF transaction state or privileged files are needed to compare EVRs.
const PackageRecord current{QStringLiteral("touchpad-hold-tap"), installed, QStringLiteral("noarch"), QStringLiteral("Gesture")};
const PackageRecord candidate{current.name, available, QStringLiteral("x86_64"), current.summary};
model.setPackages({current}, {candidate});
QCOMPARE(model.data(model.index(0), ToolModel::UpdateAvailableRole).toBool(), update);
model.setPackages({}, {candidate});
QVERIFY(!model.data(model.index(0), ToolModel::UpdateAvailableRole).toBool());
}
void incompatibleFrameworkIsOnlyListedWhenInstalled() {
if (packageCompatibilityError(QStringLiteral("framework-laptop-tools")).isEmpty()) QSKIP("Requires a non-Framework test host");
const PackageRecord package{QStringLiteral("framework-laptop-tools"), QStringLiteral("1"), QStringLiteral("x86_64"), QStringLiteral("Hardware controls")};
@@ -55,7 +87,7 @@ private Q_SLOTS:
const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-5.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
const PackageRecord available{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-6.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
model.setPackages({installed}, {available}, {QStringLiteral("touchpad-hold-tap")});
model.setPackages({installed}, {available});
QCOMPARE(model.rowCount(), 1);
const QModelIndex index = model.index(0);
@@ -67,7 +99,7 @@ private Q_SLOTS:
QCOMPARE(model.mayRemove(QStringLiteral("missing-tool")), false);
}
void doesNotGuessUpdatesFromDifferentVersionStrings()
void doesNotOfferDowngrades()
{
ToolModel model;
const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-7.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
+1
View File
@@ -2,5 +2,6 @@
name=Small tools for Fedora
baseurl=https://git.ajpanton.se/api/packages/ajp_anton/rpm/fedora/$releasever
enabled=1
metadata_expire=300
gpgcheck=1
gpgkey=https://git.ajpanton.se/api/packages/ajp_anton/rpm/repository.key
@@ -36,6 +36,12 @@ require_fedora() {
[[ ${ID:-} == fedora ]] || die "this workaround only supports Fedora Linux"
}
run_dnf() (
# The private download's umask must not make DNF's system state root-only.
umask 022
dnf5 "$@"
)
cleanup() {
[[ -n $temporary_dir ]] || return
rm -f -- "$temporary_dir/kscreenlocker.rpm"
@@ -233,7 +239,7 @@ enable_workaround() {
dnf_args=(install --assumeyes)
(( force )) && dnf_args+=(--allow-downgrade)
dnf5 "${dnf_args[@]}" "$candidate"
run_dnf "${dnf_args[@]}" "$candidate"
is_workaround_package -q kscreenlocker ||
die "DNF completed without enabling the workaround"
@@ -264,7 +270,7 @@ disable_workaround() {
printf 'This will replace the workaround with Fedora\x27s current KScreenLocker package.\n'
confirm
dnf5 \
run_dnf \
--repo=fedora,updates \
--refresh \
distro-sync \
@@ -1,6 +1,6 @@
Name: plasma-fingerprint-workaround
Version: 0.1.0
Release: 5%{?dist}
Release: 6%{?dist}
Summary: Opt-in patched KScreenLocker for fingerprint recovery after suspend
License: MIT
@@ -51,6 +51,9 @@ install -D -m 0644 %{SOURCE3} \
%{_datadir}/plasma-fingerprint-workaround/payload.conf
%changelog
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-6
- Keep private-download permissions from leaking into DNF's system state
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-5
- Simplify status queries and extend invalid-command tests
@@ -29,4 +29,23 @@ source "$payload_config"
[[ $payload_url == https://*/*.rpm ]]
[[ $payload_sha256 =~ ^[[:xdigit:]]{64}$ ]]
# Load the functions through the harmless help command, then replace DNF with
# a test function. Never invoke the package manager in this test.
(
source "$controller" --help >/dev/null
dnf5() {
[[ $(umask) == 0022 && $1 == test-argument ]]
}
umask 077
run_dnf test-argument
[[ $(umask) == 0077 ]]
dnf5() { return 23; }
if run_dnf test-argument; then
printf 'DNF failure was not propagated.\n' >&2
exit 1
else
[[ $? == 23 ]]
fi
)
printf 'Controller tests passed.\n'