From 19fdc5b67990bb75009a1fc511619bd449d559d7 Mon Sep 17 00:00:00 2001 From: Felix Ernst Date: Sat, 13 Jul 2024 15:37:31 +0000 Subject: [PATCH] Stop acting as admin when the authorization expires Previously, the authorization just silently expired without any visible change. The surprise only came when the user tried to do any action, which immediately would show a password prompt. We don't want users to randomly type passwords into password prompts unexpectedly showing up. This commit avoids that. With this change, the view container visibly de-escalates privileges and shows a message explaining what happened. A method was implemented in kio-admin to make this possible. See: https://commits.kde.org/kio-admin/a2da29289d12ef845e2c1da17ed04c59f1c47762 This commit also improves some logic around the responsibility of hiding bars and activating view containers. This is also part of my project funded by the European Commission. --- src/admin/bar.cpp | 84 +++++++++++++++++++++++++++++++-- src/admin/bar.h | 27 +++++++++-- src/admin/workerintegration.cpp | 59 ++++++++++++----------- src/admin/workerintegration.h | 10 ++-- src/dolphinviewcontainer.cpp | 41 +++++++++------- src/dolphinviewcontainer.h | 10 ++-- 6 files changed, 165 insertions(+), 66 deletions(-) diff --git a/src/admin/bar.cpp b/src/admin/bar.cpp index f2e7250ad..52bbc3772 100644 --- a/src/admin/bar.cpp +++ b/src/admin/bar.cpp @@ -7,16 +7,20 @@ #include "bar.h" +#include "dolphinviewcontainer.h" #include "workerintegration.h" #include #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -24,8 +28,14 @@ using namespace Admin; -Bar::Bar(QWidget *parent) - : AnimatedHeightWidget{parent} +namespace +{ +QPointer waitingForExpirationOfAuthorization; +} + +Bar::Bar(DolphinViewContainer *parentViewContainer) + : AnimatedHeightWidget{parentViewContainer} + , m_parentViewContainer{parentViewContainer} { setAutoFillBackground(true); updateColors(); @@ -46,8 +56,19 @@ Bar::Bar(QWidget *parent) contenntsContainer); m_closeButton->setToolTip(i18nc("@info:tooltip", "Finish acting as an administrator")); m_closeButton->setFlat(true); - connect(m_closeButton, &QAbstractButton::clicked, this, &Bar::activated); // Make sure the view connected to this bar is active before exiting admin mode. - connect(m_closeButton, &QAbstractButton::clicked, this, &WorkerIntegration::exitAdminMode); + connect(m_closeButton, &QAbstractButton::clicked, m_parentViewContainer, [this]() { + m_parentViewContainer->setActive(true); // Make sure the view connected to this bar is active before exiting admin mode. + QAction *actAsAdminAction = WorkerIntegration::actAsAdminAction(); + if (actAsAdminAction->isChecked()) { + actAsAdminAction->trigger(); + } + }); + connect(m_parentViewContainer->view(), &DolphinView::urlChanged, this, [this](const QUrl &url) { + // The bar is closely related to administrative rights, so we want to hide it instantly when we are no longer using the admin protocol. + if (url.scheme() != QStringLiteral("admin")) { + setVisible(false, WithAnimation); + } + }); QHBoxLayout *layout = new QHBoxLayout(contenntsContainer); auto contentsMargins = layout->contentsMargins(); @@ -65,8 +86,15 @@ Bar::Bar(QWidget *parent) bool Bar::event(QEvent *event) { - if (event->type() == QEvent::PaletteChange) { + switch (event->type()) { + case QEvent::PaletteChange: updateColors(); + break; + case QEvent::Show: + hideTheNextTimeAuthorizationExpires(); + break; + default: + break; } return AnimatedHeightWidget::event(event); } @@ -77,6 +105,52 @@ void Bar::resizeEvent(QResizeEvent *resizeEvent) return QWidget::resizeEvent(resizeEvent); } +void Bar::hideTheNextTimeAuthorizationExpires() +{ + if (waitingForExpirationOfAuthorization.isNull()) { + QByteArray packedArgs; + QDataStream stream(&packedArgs, QIODevice::WriteOnly); + stream << (int)1; + waitingForExpirationOfAuthorization = KIO::special(QUrl(QStringLiteral("admin:/")), packedArgs, KIO::HideProgressInfo); + waitingForExpirationOfAuthorization->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoWarningHandlingEnabled, m_parentViewContainer)); + + connect(waitingForExpirationOfAuthorization, &KJob::finished, this, [](KJob *job) { + if (job->error()) { + job->uiDelegate()->showErrorMessage(); + } + }); + } + + connect(waitingForExpirationOfAuthorization, &KJob::finished, this, [this](KJob *job) { + if (job->error()) { + return; + } + // We exit admin mode now to avoid random password prompts popping up. + QUrl viewContainerUrl = m_parentViewContainer->url(); + if (viewContainerUrl.scheme() != QStringLiteral("admin")) { + return; + } + viewContainerUrl.setScheme("file"); + m_parentViewContainer->setUrl(viewContainerUrl); + + // Explain to users that their admin authorization expired. + if (!m_reenableActAsAdminAction) { + auto actAsAdminAction = WorkerIntegration::actAsAdminAction(); + m_reenableActAsAdminAction = + new QAction{actAsAdminAction->icon(), i18nc("@action:button shown after acting as admin ended", "Act as Administrator Again"), this}; + m_reenableActAsAdminAction->setToolTip(actAsAdminAction->toolTip()); + m_reenableActAsAdminAction->setWhatsThis(actAsAdminAction->whatsThis()); + connect(m_reenableActAsAdminAction, &QAction::triggered, this, [this, actAsAdminAction]() { + m_parentViewContainer->setActive(true); + actAsAdminAction->trigger(); + }); + } + m_parentViewContainer->showMessage(i18nc("@info", "Administrator authorization has expired."), + KMessageWidget::Information, + {m_reenableActAsAdminAction}); + }); +} + void Bar::updateColors() { QPalette palette = parentWidget()->palette(); diff --git a/src/admin/bar.h b/src/admin/bar.h index 5399fa669..d1754f3bb 100644 --- a/src/admin/bar.h +++ b/src/admin/bar.h @@ -10,6 +10,7 @@ #include "animatedheightwidget.h" +class DolphinViewContainer; class QLabel; class QPushButton; class QResizeEvent; @@ -28,19 +29,24 @@ class Bar : public AnimatedHeightWidget Q_OBJECT public: - explicit Bar(QWidget *parent); + explicit Bar(DolphinViewContainer *parentViewContainer); /** Used to recolor this bar when this application's color scheme changes. */ bool event(QEvent *event) override; -Q_SIGNALS: - void activated(); - protected: /** Calls updateLabelString() */ void resizeEvent(QResizeEvent *resizeEvent) override; private: + /** + * Makes sure this admin bar hides itself when the elevated privileges expire so the user doesn't mistakenly assume that they are still acting with + * administrative rights. The view container is also changed to a non-admin url, so no password prompts will pop up unexpectedly. + * Then this method shows a message to the user to explain this. + * The mechanism of this method only fires once and will need to be called again the next time the user regains administrative rights for this view. + */ + void hideTheNextTimeAuthorizationExpires(); + /** Recolors this bar based on the current color scheme. */ void updateColors(); @@ -68,6 +74,19 @@ private: /** @see preferredHeight() */ int m_preferredHeight; + + /** + * A proxy action for the real actAsAdminAction. + * This proxy action can be used to reenable admin mode for the view belonging to this bar object specifically. + */ + QAction *m_reenableActAsAdminAction = nullptr; + + /** + * The parent of this bar. The bar acts on the DolphinViewContainer to exit the admin mode. This can happen in two ways: + * 1. The user closes the bar which implies exiting of the admin mode. + * 2. The admin authorization expires so all views can factually no longer be in admin mode. + */ + DolphinViewContainer *m_parentViewContainer; }; } diff --git a/src/admin/workerintegration.cpp b/src/admin/workerintegration.cpp index 038c52094..0b95ec782 100644 --- a/src/admin/workerintegration.cpp +++ b/src/admin/workerintegration.cpp @@ -134,43 +134,41 @@ void WorkerIntegration::createActAsAdminAction(KActionCollection *actionCollecti } } -void WorkerIntegration::exitAdminMode() -{ - if (instance->m_actAsAdminAction->isChecked()) { - instance->m_actAsAdminAction->trigger(); - } -} - void WorkerIntegration::toggleActAsAdmin() { auto dolphinMainWindow = static_cast(parent()); QUrl url = dolphinMainWindow->activeViewContainer()->urlNavigator()->locationUrl(); - if (url.scheme() == QStringLiteral("file")) { - bool risksAccepted = !KMessageBox::shouldBeShownContinue(warningDontShowAgainName); - if (!risksAccepted) { - KMessageDialog warningDialog{KMessageDialog::QuestionTwoActions, warningMessage(), dolphinMainWindow}; - warningDialog.setCaption(i18nc("@title:window", "Risks of Acting as an Administrator")); - warningDialog.setIcon(QIcon::fromTheme(QStringLiteral("security-low"))); - warningDialog.setButtons(KGuiItem{i18nc("@action:button", "I Understand and Accept These Risks"), QStringLiteral("data-warning")}, - KStandardGuiItem::cancel()); - warningDialog.setDontAskAgainText(i18nc("@option:check", "Do not warn me about these risks again")); - - risksAccepted = warningDialog.exec() != 4 /* Cancel */; - if (warningDialog.isDontAskAgainChecked()) { - KMessageBox::saveDontShowAgainContinue(warningDontShowAgainName); - } + if (url.scheme() == QStringLiteral("admin")) { + url.setScheme(QStringLiteral("file")); + dolphinMainWindow->changeUrl(url); + return; + } else if (url.scheme() != QStringLiteral("file")) { + return; + } - if (!risksAccepted) { - updateActAsAdminAction(); // Uncheck the action - return; - } + bool risksAccepted = !KMessageBox::shouldBeShownContinue(warningDontShowAgainName); + + if (!risksAccepted) { + KMessageDialog warningDialog{KMessageDialog::QuestionTwoActions, warningMessage(), dolphinMainWindow}; + warningDialog.setCaption(i18nc("@title:window", "Risks of Acting as an Administrator")); + warningDialog.setIcon(QIcon::fromTheme(QStringLiteral("security-low"))); + warningDialog.setButtons(KGuiItem{i18nc("@action:button", "I Understand and Accept These Risks"), QStringLiteral("data-warning")}, + KStandardGuiItem::cancel()); + warningDialog.setDontAskAgainText(i18nc("@option:check", "Do not warn me about these risks again")); + + risksAccepted = warningDialog.exec() != 4 /* Cancel */; + if (warningDialog.isDontAskAgainChecked()) { + KMessageBox::saveDontShowAgainContinue(warningDontShowAgainName); } - url.setScheme(QStringLiteral("admin")); - } else if (url.scheme() == QStringLiteral("admin")) { - url.setScheme(QStringLiteral("file")); + if (!risksAccepted) { + updateActAsAdminAction(); // Uncheck the action + return; + } } + + url.setScheme(QStringLiteral("admin")); dolphinMainWindow->changeUrl(url); } @@ -189,3 +187,8 @@ void WorkerIntegration::updateActAsAdminAction() } } } + +QAction *WorkerIntegration::actAsAdminAction() +{ + return instance->m_actAsAdminAction; +} diff --git a/src/admin/workerintegration.h b/src/admin/workerintegration.h index 0c87c2ecf..19cc5c172 100644 --- a/src/admin/workerintegration.h +++ b/src/admin/workerintegration.h @@ -57,11 +57,6 @@ public: */ static void createActAsAdminAction(KActionCollection *actionCollection, DolphinMainWindow *dolphinMainWindow); - /** - * Triggers the m_actAsAdminAction only if it is currently checked. - */ - static void exitAdminMode(); - private: WorkerIntegration(DolphinMainWindow *parent, QAction *actAsAdminAction); @@ -74,9 +69,14 @@ private: /** Updates the toggled/checked state of the action depending on the state of the currently active view. */ static void updateActAsAdminAction(); + /** Used by the friend class Bar to show the m_actAsAdminAction to users. */ + static QAction *actAsAdminAction(); + private: /** @see createActAsAdminAction() */ QAction *const m_actAsAdminAction = nullptr; + + friend class Bar; // Allows the bar to access the actAsAdminAction, so users can use the bar to change who they are acting as. }; } diff --git a/src/dolphinviewcontainer.cpp b/src/dolphinviewcontainer.cpp index 99ba4efa8..27f845fa7 100644 --- a/src/dolphinviewcontainer.cpp +++ b/src/dolphinviewcontainer.cpp @@ -145,7 +145,6 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) connect(m_view, &DolphinView::hiddenFilesShownChanged, this, &DolphinViewContainer::slotHiddenFilesShownChanged); connect(m_view, &DolphinView::sortHiddenLastChanged, this, &DolphinViewContainer::slotSortHiddenLastChanged); connect(m_view, &DolphinView::currentDirectoryRemoved, this, &DolphinViewContainer::slotCurrentDirectoryRemoved); - connect(m_view, &DolphinView::urlChanged, this, &DolphinViewContainer::updateAdminBarVisibility); // Initialize status bar m_statusBar = new DolphinStatusBar(this); @@ -165,7 +164,9 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) }); connect(m_statusBar, &DolphinStatusBar::stopPressed, this, &DolphinViewContainer::stopDirectoryLoading); connect(m_statusBar, &DolphinStatusBar::zoomLevelChanged, this, &DolphinViewContainer::slotStatusBarZoomLevelChanged); - connect(m_statusBar, &DolphinStatusBar::showMessage, this, &DolphinViewContainer::showMessage); + connect(m_statusBar, &DolphinStatusBar::showMessage, this, [this](const QString &message, KMessageWidget::MessageType messageType) { + showMessage(message, messageType); + }); m_statusBarTimer = new QTimer(this); m_statusBarTimer->setSingleShot(true); @@ -182,7 +183,6 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) m_topLayout->addWidget(m_statusBar, positionFor.statusBar, 0); setSearchModeEnabled(isSearchUrl(url)); - updateAdminBarVisibility(url); // Update view as the ContentDisplaySettings change // this happens here and not in DolphinView as DolphinviewContainer and DolphinView are not in the same build target ATM @@ -404,7 +404,7 @@ void DolphinViewContainer::slotSplitTabDisabled() } } -void DolphinViewContainer::showMessage(const QString &message, KMessageWidget::MessageType messageType) +void DolphinViewContainer::showMessage(const QString &message, KMessageWidget::MessageType messageType, std::initializer_list buttonActions) { if (message.isEmpty()) { return; @@ -417,6 +417,14 @@ void DolphinViewContainer::showMessage(const QString &message, KMessageWidget::M m_messageWidget->setWordWrap(true); m_messageWidget->setMessageType(messageType); + const QList previousMessageWidgetActions = m_messageWidget->actions(); + for (auto action : previousMessageWidgetActions) { + m_messageWidget->removeAction(action); + } + for (QAction *action : buttonActions) { + m_messageWidget->addAction(action); + } + m_messageWidget->setWordWrap(false); const int unwrappedWidth = m_messageWidget->sizeHint().width(); m_messageWidget->setWordWrap(unwrappedWidth > size().width()); @@ -638,6 +646,17 @@ void DolphinViewContainer::slotDirectoryLoadingCompleted() if (m_urlNavigatorConnected) { m_urlNavigatorConnected->setReadOnlyBadgeVisible(rootItem().isLocalFile() && !rootItem().isWritable()); } + + // Update admin bar visibility + if (m_view->url().scheme() == QStringLiteral("admin")) { + if (!m_adminBar) { + m_adminBar = new Admin::Bar(this); + m_topLayout->addWidget(m_adminBar, positionFor.adminBar, 0); + } + m_adminBar->setVisible(true, WithAnimation); + } else if (m_adminBar) { + m_adminBar->setVisible(false, WithAnimation); + } } void DolphinViewContainer::slotDirectoryLoadingCanceled() @@ -740,20 +759,6 @@ void DolphinViewContainer::showItemInfo(const KFileItem &item) } } -void DolphinViewContainer::updateAdminBarVisibility(const QUrl &url) -{ - if (url.scheme() == QStringLiteral("admin")) { - if (!m_adminBar) { - m_adminBar = new Admin::Bar(this); - m_topLayout->addWidget(m_adminBar, positionFor.adminBar, 0); - connect(m_adminBar, &Admin::Bar::activated, this, &DolphinViewContainer::activate); - } - m_adminBar->setVisible(true, WithAnimation); - } else if (m_adminBar) { - m_adminBar->setVisible(false, WithAnimation); - } -} - void DolphinViewContainer::closeFilterBar() { m_filterBar->closeFilterBar(); diff --git a/src/dolphinviewcontainer.h b/src/dolphinviewcontainer.h index 55d00a3b8..c5da6b48b 100644 --- a/src/dolphinviewcontainer.h +++ b/src/dolphinviewcontainer.h @@ -21,6 +21,8 @@ #include #include +#include + namespace Admin { class Bar; @@ -150,8 +152,9 @@ public: /** * Shows the message \message with the given type \messageType non-modal above the view-content. + * \buttonActions defines actions which the user can trigger as a response to this message. They are presented as buttons below the \message. */ - void showMessage(const QString &message, KMessageWidget::MessageType messageType); + void showMessage(const QString &message, KMessageWidget::MessageType messageType, std::initializer_list buttonActions = {}); /** * Refreshes the view container to get synchronized with the (updated) Dolphin settings. @@ -328,11 +331,6 @@ private Q_SLOTS: */ void showItemInfo(const KFileItem &item); - /** - * Sets the Admin::Bar visible or invisible based on whether \a url is an admin url. - */ - void updateAdminBarVisibility(const QUrl &url); - void closeFilterBar(); /** -- 2.47.3