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(Qt6 6.8 REQUIRED COMPONENTS Core Quick)
find_package(KF6 6.0 REQUIRED COMPONENTS Auth Config CoreAddons I18n KCMUtils) 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) add_subdirectory(src)
if(BUILD_TESTING) 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 package names that independently resolve to that capability in the
`fedora-tools` repository. `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 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. operations run one at a time; other actions remain disabled until DNF finishes.
The module currently configures task-group shortcut behaviour, touchpad hold-tap The module currently configures task-group shortcut behaviour, touchpad hold-tap
@@ -1,6 +1,6 @@
Name: fedora-tools-settings Name: fedora-tools-settings
Version: 0.1.0 Version: 0.1.0
Release: 13%{?dist} Release: 14%{?dist}
Summary: Plasma System Settings module for Fedora Tools Summary: Plasma System Settings module for Fedora Tools
License: MIT License: MIT
@@ -18,6 +18,7 @@ BuildRequires: kf6-kconfig-devel
BuildRequires: kf6-ki18n-devel BuildRequires: kf6-ki18n-devel
BuildRequires: kf6-kirigami-devel BuildRequires: kf6-kirigami-devel
BuildRequires: qt6-qtdeclarative-devel BuildRequires: qt6-qtdeclarative-devel
BuildRequires: rpm-devel
Requires: dnf5 Requires: dnf5
Requires: kf6-kauth Requires: kf6-kauth
@@ -74,6 +75,10 @@ done
%{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy %{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy
%changelog %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 * Sat Sep 12 2026 fedora-tools contributors - 0.1.0-13
- Share settings validation and report failed configuration writes - Share settings validation and report failed configuration writes
- Simplify ordering and source packaging - Simplify ordering and source packaging
+1
View File
@@ -10,6 +10,7 @@ kcmutils_add_qml_kcm(kcm_fedora_tools
toolmodel.h toolmodel.h
) )
target_link_libraries(kcm_fedora_tools PRIVATE target_link_libraries(kcm_fedora_tools PRIVATE
PkgConfig::RPM
Qt6::Core Qt6::Core
Qt6::Quick Qt6::Quick
KF6::AuthCore KF6::AuthCore
+7 -35
View File
@@ -142,7 +142,6 @@ void FedoraToolsKcm::refresh(bool refreshMetadata)
m_refreshMetadata = refreshMetadata; m_refreshMetadata = refreshMetadata;
m_installedPackages.clear(); m_installedPackages.clear();
m_availablePackages.clear(); m_availablePackages.clear();
m_updates.clear();
setMessage({}); setMessage({});
setBusy(true); setBusy(true);
@@ -316,8 +315,12 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
const QByteArray output = m_query.readAllStandardOutput(); const QByteArray output = m_query.readAllStandardOutput();
const QByteArray errorOutput = m_query.readAllStandardError(); const QByteArray errorOutput = m_query.readAllStandardError();
if (exitStatus != QProcess::NormalExit) { // rpm -q reports missing packages on stdout with exit code 1. That is a
setMessage(i18n("Package discovery terminated unexpectedly."), true); // 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; m_stage = QueryStage::Idle;
setPackageOperation({}, {}); setPackageOperation({}, {});
setBusy(false); setBusy(false);
@@ -342,43 +345,12 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
} }
if (m_stage == QueryStage::Available) { 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); m_availablePackages = parsePackageRecords(output);
if (m_availablePackages.isEmpty()) { if (m_availablePackages.isEmpty()) {
finishRefresh(); finishRefresh();
return; 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; QStringList packageNames;
for (const PackageRecord &package : std::as_const(m_availablePackages)) { for (const PackageRecord &package : std::as_const(m_availablePackages)) {
packageNames.append(package.name); packageNames.append(package.name);
@@ -407,7 +379,7 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
void FedoraToolsKcm::finishRefresh() void FedoraToolsKcm::finishRefresh()
{ {
m_tools.setPackages(m_installedPackages, m_availablePackages, m_updates); m_tools.setPackages(m_installedPackages, m_availablePackages);
updateFingerprintStatus(); updateFingerprintStatus();
m_stage = QueryStage::Idle; m_stage = QueryStage::Idle;
setPackageOperation({}, {}); setPackageOperation({}, {});
@@ -8,7 +8,6 @@
#include <KQuickConfigModule> #include <KQuickConfigModule>
#include <QProcess> #include <QProcess>
#include <QSet>
#include <QUrl> #include <QUrl>
class KJob; class KJob;
@@ -75,7 +74,6 @@ private:
Idle, Idle,
InstalledProviders, InstalledProviders,
Available, Available,
Updates,
KnownInstalled, KnownInstalled,
}; };
@@ -95,7 +93,6 @@ private:
QueryStage m_stage = QueryStage::Idle; QueryStage m_stage = QueryStage::Idle;
QList<PackageRecord> m_installedPackages; QList<PackageRecord> m_installedPackages;
QList<PackageRecord> m_availablePackages; QList<PackageRecord> m_availablePackages;
QSet<QString> m_updates;
bool m_refreshMetadata = false; bool m_refreshMetadata = false;
bool m_busy = false; bool m_busy = false;
bool m_error = false; bool m_error = false;
+17 -3
View File
@@ -10,12 +10,26 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QStandardPaths> #include <QStandardPaths>
#include <rpm/rpmver.h>
#include <algorithm> #include <algorithm>
#include <utility> #include <utility>
namespace 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) QStringList readConfigurationCommand(const QString &packageName)
{ {
const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
@@ -119,8 +133,7 @@ QHash<int, QByteArray> ToolModel::roleNames() const
} }
void ToolModel::setPackages(const QList<PackageRecord> &installed, void ToolModel::setPackages(const QList<PackageRecord> &installed,
const QList<PackageRecord> &available, const QList<PackageRecord> &available)
const QSet<QString> &updates)
{ {
QMap<QString, Tool> tools; QMap<QString, Tool> tools;
@@ -147,7 +160,8 @@ void ToolModel::setPackages(const QList<PackageRecord> &installed,
} }
tool.compatibilityError = incompatible; tool.compatibilityError = incompatible;
tool.available = incompatible.isEmpty(); 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(); beginResetModel();
+1 -3
View File
@@ -5,7 +5,6 @@
#include "packageutils.h" #include "packageutils.h"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QSet>
#include <QStringList> #include <QStringList>
struct Tool struct Tool
@@ -48,8 +47,7 @@ public:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
void setPackages(const QList<PackageRecord> &installed, void setPackages(const QList<PackageRecord> &installed,
const QList<PackageRecord> &available, const QList<PackageRecord> &available);
const QSet<QString> &updates = {});
bool mayInstall(const QString &packageName) const; bool mayInstall(const QString &packageName) const;
bool mayRemove(const QString &packageName) const; bool mayRemove(const QString &packageName) const;
QStringList configurationCommand(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 ../src/packageutils.cpp
) )
target_include_directories(test-toolmodel PRIVATE ../src) 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_test(NAME toolmodel COMMAND test-toolmodel)
add_executable(test-qmlresource test-qmlresource.cpp) add_executable(test-qmlresource test-qmlresource.cpp)
+34 -2
View File
@@ -12,6 +12,38 @@ class ToolModelTest : public QObject
Q_OBJECT Q_OBJECT
private Q_SLOTS: 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() { void incompatibleFrameworkIsOnlyListedWhenInstalled() {
if (packageCompatibilityError(QStringLiteral("framework-laptop-tools")).isEmpty()) QSKIP("Requires a non-Framework test host"); 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")}; 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 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")}; 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); QCOMPARE(model.rowCount(), 1);
const QModelIndex index = model.index(0); const QModelIndex index = model.index(0);
@@ -67,7 +99,7 @@ private Q_SLOTS:
QCOMPARE(model.mayRemove(QStringLiteral("missing-tool")), false); QCOMPARE(model.mayRemove(QStringLiteral("missing-tool")), false);
} }
void doesNotGuessUpdatesFromDifferentVersionStrings() void doesNotOfferDowngrades()
{ {
ToolModel model; ToolModel model;
const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-7.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")}; 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 name=Small tools for Fedora
baseurl=https://git.ajpanton.se/api/packages/ajp_anton/rpm/fedora/$releasever baseurl=https://git.ajpanton.se/api/packages/ajp_anton/rpm/fedora/$releasever
enabled=1 enabled=1
metadata_expire=300
gpgcheck=1 gpgcheck=1
gpgkey=https://git.ajpanton.se/api/packages/ajp_anton/rpm/repository.key 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" [[ ${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() { cleanup() {
[[ -n $temporary_dir ]] || return [[ -n $temporary_dir ]] || return
rm -f -- "$temporary_dir/kscreenlocker.rpm" rm -f -- "$temporary_dir/kscreenlocker.rpm"
@@ -233,7 +239,7 @@ enable_workaround() {
dnf_args=(install --assumeyes) dnf_args=(install --assumeyes)
(( force )) && dnf_args+=(--allow-downgrade) (( force )) && dnf_args+=(--allow-downgrade)
dnf5 "${dnf_args[@]}" "$candidate" run_dnf "${dnf_args[@]}" "$candidate"
is_workaround_package -q kscreenlocker || is_workaround_package -q kscreenlocker ||
die "DNF completed without enabling the workaround" 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' printf 'This will replace the workaround with Fedora\x27s current KScreenLocker package.\n'
confirm confirm
dnf5 \ run_dnf \
--repo=fedora,updates \ --repo=fedora,updates \
--refresh \ --refresh \
distro-sync \ distro-sync \
@@ -1,6 +1,6 @@
Name: plasma-fingerprint-workaround Name: plasma-fingerprint-workaround
Version: 0.1.0 Version: 0.1.0
Release: 5%{?dist} Release: 6%{?dist}
Summary: Opt-in patched KScreenLocker for fingerprint recovery after suspend Summary: Opt-in patched KScreenLocker for fingerprint recovery after suspend
License: MIT License: MIT
@@ -51,6 +51,9 @@ install -D -m 0644 %{SOURCE3} \
%{_datadir}/plasma-fingerprint-workaround/payload.conf %{_datadir}/plasma-fingerprint-workaround/payload.conf
%changelog %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 * Sat Sep 12 2026 fedora-tools contributors - 0.1.0-5
- Simplify status queries and extend invalid-command tests - Simplify status queries and extend invalid-command tests
@@ -29,4 +29,23 @@ source "$payload_config"
[[ $payload_url == https://*/*.rpm ]] [[ $payload_url == https://*/*.rpm ]]
[[ $payload_sha256 =~ ^[[:xdigit:]]{64}$ ]] [[ $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' printf 'Controller tests passed.\n'