1
0
mirror of https://invent.kde.org/system/dolphin synced 2024-07-07 10:51:45 +00:00

Guide users to using kio-admin instead of sudo

This commit adds a guided setup that leads users from a situation
in which they try to "sudo dolphin" towards them successfully
setting up and using kio-admin.

1. When users enter "sudo dolphin", they are told to start Dolphin
   by typing "dolphin --sudo" or "dolphin --admin" instead.
2. When Dolphin is started with "--sudo" or "--admin" it checks
   whether an "admin" protocol is installed. If not, a guided
   setup leads users towards installing it.
3. After that, Dolphin starts with an installed "admin" protocoll
   like kio-admin. Now a non-modal information dialog appears that
   explains how to activate and use kio-admin.
This commit is contained in:
Felix Ernst 2024-07-01 12:03:22 +00:00
parent 887f3a6e83
commit 92b178b740
8 changed files with 332 additions and 19 deletions

View File

@ -103,7 +103,7 @@ find_package(Phonon4Qt6 CONFIG REQUIRED)
find_package(PackageKitQt6)
set_package_properties(PackageKitQt6
PROPERTIES DESCRIPTION "Software Manager integration"
TYPE OPTIONAL
TYPE RECOMMENDED
PURPOSE "Used in the service menu installer"
)
if(PackageKitQt6_FOUND)

View File

@ -1,5 +1,6 @@
include(ECMAddAppIcon)
set(ADMIN_WORKER_PACKAGE_NAME "kio-admin")
configure_file(config-dolphin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-dolphin.h)
add_definitions(
@ -274,6 +275,7 @@ target_sources(dolphinstatic PRIVATE
dolphincontextmenu.cpp
dolphinnavigatorswidgetaction.cpp
dolphintabbar.cpp
dolphinpackageinstaller.cpp
dolphinplacesmodelsingleton.cpp
dolphinrecenttabsmenu.cpp
dolphintabpage.cpp
@ -334,6 +336,7 @@ target_sources(dolphinstatic PRIVATE
dolphincontextmenu.h
dolphinnavigatorswidgetaction.h
dolphintabbar.h
dolphinpackageinstaller.h
dolphinplacesmodelsingleton.h
dolphinrecenttabsmenu.h
dolphintabpage.h
@ -459,6 +462,13 @@ if (HAVE_PLASMA_ACTIVITIES)
)
endif()
if(HAVE_PACKAGEKIT)
target_link_libraries(
dolphinstatic
PK::packagekitqt6
)
endif()
if (HAVE_KUSERFEEDBACK)
target_link_libraries(
dolphinstatic

View File

@ -7,7 +7,9 @@
#include "workerintegration.h"
#include "config-dolphin.h"
#include "dolphinmainwindow.h"
#include "dolphinpackageinstaller.h"
#include "dolphinviewcontainer.h"
#include <KActionCollection>
@ -18,8 +20,77 @@
#include <QAction>
#include <iostream>
using namespace Admin;
/** Free file-local functions */
namespace
{
/** @returns the translated name of the actAsAdminAction. */
QString actionName()
{
return i18nc("@action:inmenu", "Act as Administrator");
};
/** @returns the default keyboard shortcut of the actAsAdminAction. */
QKeySequence actionDefaultShortcut()
{
return Qt::CTRL | Qt::SHIFT | Qt::ALT | Qt::Key_A;
};
/** @returns whether any worker for the protocol "admin" is available. */
bool isWorkerInstalled()
{
return KProtocolInfo::isKnownProtocol(QStringLiteral("admin"));
}
}
void Admin::guideUserTowardsInstallingAdminWorker()
{
if (!isWorkerInstalled()) {
std::cout << qPrintable(
xi18nc("@info:shell",
"<application>Dolphin</application> requires <application>%1</application> to manage system-controlled files, but it is not installed.<nl/>"
"Press %2 to install <application>%1</application> or %3 to cancel.",
ADMIN_WORKER_PACKAGE_NAME,
QKeySequence{Qt::Key_Enter}.toString(QKeySequence::NativeText),
QKeySequence{Qt::CTRL | Qt::Key_C}.toString(QKeySequence::NativeText)));
std::cin.ignore();
/// Installing admin worker
DolphinPackageInstaller adminWorkerInstaller{ADMIN_WORKER_PACKAGE_NAME, QUrl(QStringLiteral("appstream://org.kde.kio.admin")), isWorkerInstalled};
QObject::connect(&adminWorkerInstaller, &KJob::result, [](KJob *job) {
if (job->error()) {
std::cout << qPrintable(job->errorString()) << std::endl;
exit(1);
}
});
adminWorkerInstaller.exec();
}
}
void Admin::guideUserTowardsUsingAdminWorker()
{
KuitSetup *kuitSetup = &Kuit::setupForDomain("dolphin");
kuitSetup->setTagPattern(QStringLiteral("numberedlist"), QStringList{}, Kuit::RichText, ki18nc("tag-format-pattern <numberedlist> rich", "<ol>%1</ol>"));
kuitSetup->setTagPattern(QStringLiteral("numbereditem"), QStringList{}, Kuit::RichText, ki18nc("tag-format-pattern <numbereditem> rich", "<li>%1</li>"));
KMessageBox::information(
nullptr,
xi18nc("@info",
"<para>Make use of your administrator rights in Dolphin:<numberedlist>"
"<numbereditem>Navigate to the file or folder you want to change.</numbereditem>"
"<numbereditem>Activate the \"%1\" action either under <interface>Open Menu|More|View</interface> or <interface>Menu Bar|View</interface>.<nl/>"
"Default shortcut: <shortcut>%2</shortcut></numbereditem>"
"<numbereditem>After authorization you can manage files as an administrator.</numbereditem></numberedlist></para>",
actionName(),
actionDefaultShortcut().toString(QKeySequence::NativeText)),
i18nc("@title:window", "How to Administrate"),
"",
KMessageBox::WindowModal);
}
QString Admin::warningMessage()
{
return xi18nc(
@ -52,12 +123,12 @@ WorkerIntegration::WorkerIntegration(DolphinMainWindow *parent, QAction *actAsAd
void WorkerIntegration::createActAsAdminAction(KActionCollection *actionCollection, DolphinMainWindow *dolphinMainWindow)
{
Q_ASSERT(!instance);
if (KProtocolInfo::isKnownProtocol(QStringLiteral("admin"))) {
if (isWorkerInstalled()) {
QAction *actAsAdminAction = actionCollection->addAction(QStringLiteral("act_as_admin"));
actAsAdminAction->setText(i18nc("@action:inmenu", "Act as Administrator"));
actAsAdminAction->setText(actionName());
actAsAdminAction->setIcon(QIcon::fromTheme(QStringLiteral("system-switch-user")));
actAsAdminAction->setCheckable(true);
actionCollection->setDefaultShortcut(actAsAdminAction, Qt::CTRL | Qt::SHIFT | Qt::ALT | Qt::Key_A);
actionCollection->setDefaultShortcut(actAsAdminAction, actionDefaultShortcut());
instance = new WorkerIntegration(dolphinMainWindow, actAsAdminAction);
}

View File

@ -22,6 +22,15 @@ class QUrl;
*/
namespace Admin
{
/**
* When a user starts Dolphin with arguments that imply that they want to use administrative rights, this method is called.
* This function acts like a command line program that guides users towards installing kio-admin. It will not return until this is accomplished.
* This function will do nothing if kio-admin is already installed.
*/
void guideUserTowardsInstallingAdminWorker();
void guideUserTowardsUsingAdminWorker();
/**
* Used with the KMessageBox API so users can disable the warning.
* @see KMessageBox::saveDontShowAgainContinue()

View File

@ -1,6 +1,10 @@
/** Set whether to build Dolphin with support for these technologies or not. */
#cmakedefine01 HAVE_BALOO
#cmakedefine01 HAVE_PLASMA_ACTIVITIES
#cmakedefine01 HAVE_KUSERFEEDBACK
#cmakedefine01 HAVE_PACKAGEKIT
#cmakedefine01 HAVE_TERMINAL
#cmakedefine01 HAVE_X11
/** The name of the package that needs to be installed so URLs starting with "admin:" can be opened in Dolphin. */
#cmakedefine ADMIN_WORKER_PACKAGE_NAME "@ADMIN_WORKER_PACKAGE_NAME@"

View File

@ -0,0 +1,115 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "dolphinpackageinstaller.h"
#include <KLocalizedString>
#if HAVE_PACKAGEKIT
#include <PackageKit/Daemon>
#else
#include <QDesktopServices>
#endif
#include <QTimer>
#include <QtAssert>
DolphinPackageInstaller::DolphinPackageInstaller(const QString &packageName,
const QUrl &fallBackInstallationPageUrl,
std::function<bool()> isPackageInstalledCheck,
QObject *parent)
: KJob(parent)
, m_packageName{packageName}
, m_fallBackInstallationPageUrl{fallBackInstallationPageUrl}
, m_isPackageInstalledCheck{isPackageInstalledCheck}
{
}
void DolphinPackageInstaller::start()
{
if (m_isPackageInstalledCheck()) {
emitResult();
return;
}
#if HAVE_PACKAGEKIT
PackageKit::Daemon::setHints(PackageKit::Daemon::hints() + QStringList{QStringLiteral("interactive=true")});
const PackageKit::Transaction *resolveTransaction = PackageKit::Daemon::resolve(m_packageName);
connect(resolveTransaction, &PackageKit::Transaction::errorCode, this, &DolphinPackageInstaller::slotInstallationFailed);
connect(resolveTransaction, &PackageKit::Transaction::finished, this, [this]() { // Will be disconnected if we find a package.
slotInstallationFailed(PackageKit::Transaction::ErrorPackageNotFound,
i18nc("@info:shell about system packages", "Could not find package %1.", m_packageName));
});
connect(resolveTransaction,
&PackageKit::Transaction::package,
this,
[this, resolveTransaction](PackageKit::Transaction::Info /* info */, const QString &packageId) {
disconnect(resolveTransaction, nullptr, this, nullptr); // We only care about the first package.
install(packageId);
});
#else
QDesktopServices::openUrl(m_fallBackInstallationPageUrl);
auto waitForSuccess = new QTimer(this);
connect(waitForSuccess, &QTimer::timeout, this, [this]() {
if (m_isPackageInstalledCheck()) {
emitResult();
}
});
waitForSuccess->start(3000);
#endif
}
#if HAVE_PACKAGEKIT
void DolphinPackageInstaller::install(const QString &packageId)
{
const PackageKit::Transaction *installTransaction = PackageKit::Daemon::installPackage(packageId);
connectTransactionToJobProgress(*installTransaction);
connect(installTransaction,
&PackageKit::Transaction::errorCode,
this,
[installTransaction, this](PackageKit::Transaction::Error error, const QString &details) {
disconnect(installTransaction, nullptr, this, nullptr); // We only want to emit a result once.
slotInstallationFailed(error, details);
});
connect(installTransaction,
&PackageKit::Transaction::finished,
this,
[installTransaction, this](const PackageKit::Transaction::Exit status, uint /* runtime */) {
disconnect(installTransaction, nullptr, this, nullptr); // We only want to emit a result once.
if (status == PackageKit::Transaction::ExitSuccess) {
emitResult();
} else {
slotInstallationFailed(PackageKit::Transaction::ErrorUnknown,
i18nc("@info %1 is error code",
"Installation exited without reporting success. (%1)",
QMetaEnum::fromType<PackageKit::Transaction::Exit>().valueToKey(status)));
}
});
}
void DolphinPackageInstaller::connectTransactionToJobProgress(const PackageKit::Transaction &transaction)
{
connect(&transaction, &PackageKit::Transaction::speedChanged, this, [this, &transaction]() {
emitSpeed(transaction.speed());
});
connect(&transaction, &PackageKit::Transaction::percentageChanged, this, [this, &transaction]() {
setPercent(transaction.percentage());
});
}
void DolphinPackageInstaller::slotInstallationFailed(PackageKit::Transaction::Error error, const QString &details)
{
setErrorString(xi18nc("@info:shell %1 is package name, %2 is error message, %3 is error e.g. 'ErrorNoNetwork'",
"Installing <application>%1</application> failed: %2 (%3)<nl/>Please try installing <application>%1</application> manually instead.",
m_packageName,
details,
QMetaEnum::fromType<PackageKit::Transaction::Error>().valueToKey(error)));
setError(error);
emitResult();
}
#endif

View File

@ -0,0 +1,92 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef dolphinpackageinstaller_H
#define dolphinpackageinstaller_H
#include "config-dolphin.h"
#if HAVE_PACKAGEKIT
#include <PackageKit/Transaction>
#endif
#include <KJob>
#include <QUrl>
/**
* @brief A KJob providing simple means to install a package.
*/
class DolphinPackageInstaller : public KJob
{
public:
/**
* @brief Installs a system package.
*
* @param packageName A name that can be resolved to a package.
* @param fallBackInstallationPageUrl This url will be opened if Dolphin was installed without the PackageKit library. A good choice for this parameter
* is an appstream url that will be opened in a software store like Discover
* e.g. "appstream://org.kde.filelight.desktop". The user is then expected to install the package themselves and
* KJob::result() will be emitted when it is detected that the installation finished successfully.
* @param isPackageInstalledCheck A function that can be regularly checked to determine if the installation was already successful.
*/
explicit DolphinPackageInstaller(const QString &packageName,
const QUrl &fallBackInstallationPageUrl,
std::function<bool()> isPackageInstalledCheck,
QObject *parent = nullptr);
/**
* @see KJob::start().
* Make sure to connect to the KJob::result() signal and show the KJob::errorString() to users there before calling this.
*/
void start() override;
/** @see KJob::errorString(). */
inline QString errorString() const override
{
return m_errorString;
};
private:
/** @see KJob::errorString(). */
inline void setErrorString(const QString &errorString)
{
m_errorString = errorString;
};
#if HAVE_PACKAGEKIT
/**
* Asynchronously installs a package uniquely identified by its @param packageId using PackageKit.
* For progress reporting this method will use DolphinPackageInstaller::connectTransactionToJobProgress().
* This method will call KJob::emitResult() once it failed or succeeded.
*/
void install(const QString &packageId);
/**
* Makes sure progress signals of @p transaction are forwarded to KJob's progress signals.
*/
void connectTransactionToJobProgress(const PackageKit::Transaction &transaction);
private Q_SLOTS:
/** Creates a nice user-facing error message from its parameters and then finishes this job with an @p error. */
void slotInstallationFailed(PackageKit::Transaction::Error error, const QString &details);
#endif
private:
/** The name of the package that is supposed to be installed. */
const QString m_packageName;
/** @see DolphinPackageInstaller::DolphinPackageInstaller(). */
const QUrl m_fallBackInstallationPageUrl;
/** @see DolphinPackageInstaller::DolphinPackageInstaller(). */
const std::function<bool()> m_isPackageInstalledCheck;
/** @see KJob::errorString(). */
QString m_errorString;
};
#endif // dolphinpackageinstaller_H

View File

@ -6,6 +6,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "admin/workerintegration.h"
#include "config-dolphin.h"
#include "dbusinterface.h"
#include "dolphin_generalsettings.h"
@ -54,24 +55,22 @@
#endif
#include <iostream>
constexpr auto dolphinTranslationDomain{"dolphin"};
int main(int argc, char **argv)
{
#ifndef Q_OS_WIN
// Prohibit using sudo or kdesu (but allow using the root user directly)
if (getuid() == 0) {
if (!qEnvironmentVariableIsEmpty("SUDO_USER")) {
std::cout << "Running Dolphin with sudo is not supported as it can cause bugs and expose you to security vulnerabilities. Instead, install the "
"`kio-admin` package from your distro and use it to manage root-owned locations by right-clicking on them and selecting \"Open as "
"Administrator\"."
<< std::endl;
return EXIT_FAILURE;
} else if (!qEnvironmentVariableIsEmpty("KDESU_USER")) {
std::cout << "Running Dolphin with kdesu is not supported as it can cause bugs and expose you to security vulnerabilities. Instead, install the "
"`kio-admin` package from your distro and use it to manage root-owned locations by right-clicking on them and selecting \"Open as "
"Administrator\"."
<< std::endl;
return EXIT_FAILURE;
}
if (getuid() == 0 && (!qEnvironmentVariableIsEmpty("SUDO_USER") || !qEnvironmentVariableIsEmpty("KDESU_USER"))) {
QCoreApplication app(argc, argv); // Needed for the xi18ndc() call below.
std::cout << qPrintable(
xi18ndc(dolphinTranslationDomain,
"@info:shell %1 is a terminal command",
"Running <application>Dolphin</application> with <command>sudo</command> is discouraged. Please run <icode>%1</icode> instead.",
QStringLiteral("dolphin --sudo")))
<< std::endl;
// We could perform a privilege de-escalation here and continue as normal. It is a bit safer though to simply let the user restart without sudo.
return EXIT_FAILURE;
}
#endif
@ -116,7 +115,7 @@ int main(int argc, char **argv)
migrate.migrate();
#endif
KLocalizedString::setApplicationDomain("dolphin");
KLocalizedString::setApplicationDomain(dolphinTranslationDomain);
KAboutData aboutData(QStringLiteral("dolphin"),
i18n("Dolphin"),
@ -164,6 +163,8 @@ int main(int argc, char **argv)
"will be selected.")));
parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("split"), i18nc("@info:shell", "Dolphin will get started with a split view.")));
parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("new-window"), i18nc("@info:shell", "Dolphin will explicitly open in a new window.")));
parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("sudo") << QStringLiteral("admin"),
i18nc("@info:shell", "Set up Dolphin for administrative tasks.")));
parser.addOption(
QCommandLineOption(QStringList() << QStringLiteral("daemon"), i18nc("@info:shell", "Start Dolphin Daemon (only required for DBus Interface).")));
parser.addPositionalArgument(QStringLiteral("+[Url]"), i18nc("@info:shell", "Document to open"));
@ -173,11 +174,18 @@ int main(int argc, char **argv)
const bool splitView = parser.isSet(QStringLiteral("split")) || GeneralSettings::splitView();
const bool openFiles = parser.isSet(QStringLiteral("select"));
const bool adminWorkerInfoWanted = parser.isSet(QStringLiteral("sudo")) || parser.isSet(QStringLiteral("admin"));
const QStringList args = parser.positionalArguments();
QList<QUrl> urls = Dolphin::validateUris(args);
// We later mutate urls, so we need to store if it was empty originally
const bool startedWithURLs = !urls.isEmpty();
if (adminWorkerInfoWanted || std::any_of(urls.cbegin(), urls.cend(), [](const QUrl &url) {
return url.scheme() == QStringLiteral("admin");
})) {
Admin::guideUserTowardsInstallingAdminWorker();
}
if (parser.isSet(QStringLiteral("daemon"))) {
// Disable session management for the daemonized version
// See https://bugs.kde.org/show_bug.cgi?id=417219
@ -275,6 +283,10 @@ int main(int argc, char **argv)
mainWindow->setSessionAutoSaveEnabled(GeneralSettings::rememberOpenedTabs());
if (adminWorkerInfoWanted) {
Admin::guideUserTowardsUsingAdminWorker();
}
#if HAVE_KUSERFEEDBACK
auto feedbackProvider = DolphinFeedbackProvider::instance();
Q_UNUSED(feedbackProvider)