Commit 371c590c authored by Harald Sitter's avatar Harald Sitter 🌈
Browse files

add a group management page

to edit usershares the user needs to be member of the appropriate group.
detect when that is not the case (dir is not writable) and offer
automatic group management if at all viable (dir isn't group owned by
root and the group looks like something samba related).

to visualize this there's a new group page which is run after
installation and also on every start (it does a fairly cheap check;
never the less it is equipped with busy indication).

should the user not be able to write the usershare directory the user
can ask us to ask the kauth helper to make the necessary changes.

the entire backing logic is inside a groupmanager helper class that
evaluates the group status of the user

this also moves the reboot function back out of the samba installer into
the plugin. the group management also needs to reboot (or at least
re-log) to apply the changes so we need a more global way to issue
reboots

BUG: 407846
FIXED-IN: 20.12
parent 028e4f0e
......@@ -4,6 +4,7 @@ set(sambausershareplugin_PART_SRCS
sambausershareplugin.cpp
model.cpp
usermanager.cpp
groupmanager.cpp
)
if(SAMBA_INSTALL)
......
......@@ -69,4 +69,51 @@ ActionReply AuthHelper::createuser(const QVariantMap &args)
return reply;
}
ActionReply AuthHelper::addtogroup(const QVariantMap &args)
{
const auto user = args.value(QStringLiteral("user")).toString();
const auto group = args.value(QStringLiteral("group")).toString();
if (user.isEmpty() || group.isEmpty()) {
return ActionReply::HelperErrorReply();
}
// Harden against some input abuse.
// TODO: add ability to resolve remote UID via KAuth and verify the request (or even reduce the arguments down to
// only the group and resolve the UID)
if (!group.contains(QLatin1String("samba")) || group.contains(QLatin1String("admin")) ||
group.contains(QLatin1String("root"))) {
return ActionReply::HelperErrorReply();
}
QProcess p;
#if defined(Q_OS_FREEBSD)
p.setProgram(QStringLiteral("pw"));
p.setArguments({
QStringLiteral("group"),
QStringLiteral("mod"),
QStringLiteral("{%1}").arg(group),
QStringLiteral("-m"),
QStringLiteral("{%1}").arg(user) });
#elif defined(Q_OS_LINUX)
p.setProgram(QStringLiteral("/usr/sbin/usermod"));
p.setArguments({
QStringLiteral("--append"),
QStringLiteral("--groups"),
group,
user });
#else
# error "Platform lacks group management support. Please add support."
#endif
p.start();
p.waitForFinished(1000);
if (p.exitCode() != 0 || p.exitStatus() != QProcess::NormalExit) {
auto reply = ActionReply::HelperErrorReply();
reply.setErrorDescription(QString::fromUtf8(p.readAll()));
return reply;
}
return ActionReply::SuccessReply();
}
KAUTH_HELPER_MAIN("org.kde.filesharing.samba", AuthHelper)
......@@ -16,6 +16,7 @@ class AuthHelper: public QObject
public Q_SLOTS:
ActionReply isuserknown(const QVariantMap &args);
ActionReply createuser(const QVariantMap &args);
ActionReply addtogroup(const QVariantMap &args);
};
#endif // AUTHHELPER_H
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
*/
#include "groupmanager.h"
#include <QProcess>
#include <QFileInfo>
#include <QDebug>
#include <KUser>
#include <KAuth/KAuthAction>
#include <KAuth/KAuthExecuteJob>
#include <KLocalizedString>
GroupManager::GroupManager(QObject *parent)
: QObject(parent)
{
metaObject()->invokeMethod(this, [this] {
auto proc = new QProcess;
proc->setProgram(QStringLiteral("testparm"));
proc->setArguments({QStringLiteral("--debuglevel=0"),
QStringLiteral("--suppress-prompt"),
QStringLiteral("--verbose"),
QStringLiteral("--parameter-name"),
QStringLiteral("usershare path")});
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this, proc](int exitCode) {
proc->deleteLater();
const QString path = QString::fromUtf8(proc->readAllStandardOutput().simplified());
m_ready = true;
Q_EMIT isReadyChanged();
QFileInfo info(path);
if (exitCode != 0 || path.isEmpty() || !info.exists()) {
return; // usershares may be disabled or path is invalid :|
}
if (info.isWritable()) {
m_isMember = true;
Q_EMIT isMemberChanged();
}
m_targetGroup = info.group();
Q_EMIT targetGroupChanged();
if (m_targetGroup != QLatin1String("root") && m_targetGroup.contains(QLatin1String("samba"))) {
m_canMakeMember = true;
Q_EMIT canMakeMemberChanged();
}
});
proc->start();
});
}
bool GroupManager::canMakeMember() const
{
return m_canMakeMember;
}
bool GroupManager::isReady() const
{
return m_ready;
}
QString GroupManager::targetGroup() const
{
return m_targetGroup;
}
bool GroupManager::isMember() const
{
return m_isMember;
}
void GroupManager::makeMember()
{
Q_ASSERT(m_canMakeMember);
const QString user = KUser().loginName();
const QString group = m_targetGroup;
Q_ASSERT(!user.isEmpty());
Q_ASSERT(!group.isEmpty());
auto action = KAuth::Action(QStringLiteral("org.kde.filesharing.samba.addtogroup"));
action.setHelperId(QStringLiteral("org.kde.filesharing.samba"));
action.addArgument(QStringLiteral("user"), user);
action.addArgument(QStringLiteral("group"), group);
action.setDetailsV2({{KAuth::Action::AuthDetail::DetailMessage,
i18nc("@label kauth action description %1 is a username %2 a group name",
"Adding user '%1' to group '%2' so they may configure Samba user shares",
user,
group) }
});
KAuth::ExecuteJob *job = action.execute();
connect(job, &KAuth::ExecuteJob::result, this, [this, job] {
job->deleteLater();
if (job->error() != KAuth::ExecuteJob::NoError) {
Q_EMIT makeMemberError(job->errorString());
return;
}
Q_EMIT madeMember();
});
job->start();
}
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
*/
#pragma once
#include <QObject>
class GroupManager : public QObject
{
Q_OBJECT
Q_PROPERTY(bool ready READ isReady NOTIFY isReadyChanged)
Q_PROPERTY(bool member READ isMember NOTIFY isMemberChanged)
Q_PROPERTY(bool canMakeMember READ canMakeMember NOTIFY canMakeMemberChanged)
Q_PROPERTY(QString targetGroup READ targetGroup NOTIFY targetGroupChanged)
public:
explicit GroupManager(QObject *parent = nullptr);
bool canMakeMember() const;
bool isReady() const;
QString targetGroup() const;
bool isMember() const;
public Q_SLOTS:
void makeMember();
Q_SIGNALS:
void isReadyChanged();
void isMemberChanged();
void canMakeMemberChanged();
void madeMember();
void targetGroupChanged();
void makeMemberError(const QString &error);
private:
bool m_canMakeMember = false;
bool m_isMember = false;
bool m_ready = false;
QString m_targetGroup;
};
......@@ -68,3 +68,9 @@ Description[sl]=Ustvarjanje novega uporabnika Sambe
Description[sv]=Skapar ny Samba-användare
Description[uk]=Створення нового користувача Samba
Description[x-test]=xxCreating new Samba userxx
[org.kde.filesharing.samba.addtogroup]
Policy=auth_admin
PolicyInactive=auth_admin
Name=Add User to Samba's Usershare Group
Description=Adding user to Samba's usershare group to edit Samba usershares
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
*/
import QtQuick 2.12
import QtQuick.Controls 2.5 as QQC2
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.filesharing.samba 1.0 as Samba
Item {
// This page may be after the InstallPage and so we need to create a GroupManager when the page loads
// rather than relying on the global instance that runs from the get go so we can evaluate the status AFTER
// samba installation.
Samba.GroupManager {
id: manager
onReadyChanged: {
if (ready && member) { // already member nothing to do for us
stackReplace(pendingStack.pop())
}
}
onMadeMember: {
stackReplace("RebootPage.qml")
}
onMakeMemberError: {
var text = error
if (text == "") { // unknown error :(
text = i18nc("@label failed to change user groups so they can manage shares",
"Group changes failed.")
}
errorMessage.text = text
}
}
QQC2.BusyIndicator {
anchors.centerIn: parent
visible: !manager.ready
running: visible
}
ColumnLayout {
anchors.fill: parent
visible: manager.ready
Kirigami.InlineMessage {
id: errorMessage
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
type: Kirigami.MessageType.Error
visible: text != ""
}
Kirigami.PlaceholderMessage {
text: manager.targetGroup ?
xi18nc("@label",
"To manage Samba user shares you need to be member of the <resource>%1</resource> group.",
manager.targetGroup) :
i18nc("@label", "You appear to not have sufficient permissions to manage Samba user shares.")
helpfulAction: Kirigami.Action {
enabled: manager.canMakeMember
iconName: "resource-group-new"
text: i18nc("@button makes user a member of the samba share group", "Make me a Group Member")
onTriggered: manager.makeMember()
}
}
}
}
......@@ -17,7 +17,12 @@ Kirigami.PlaceholderMessage {
if (!installer.installed) {
return
}
stack.push("RebootPage.qml", { "installer": this })
// Installation is a bit special because it eventually ends in a reboot. So we push that page onto the
// pending pages and move to the group page. The group page in turn will either explicitly
// go to the reboot page (if group changes were made) or pop a pending page if groups are already cool.
// In either event it'll end up on the reboot page because of our pending meddling here.
pendingStack.push("RebootPage.qml")
stackReplace("GroupPage.qml")
}
}
......
......@@ -10,12 +10,10 @@ import org.kde.kirigami 2.12 as Kirigami
import org.kde.filesharing.samba 1.0 as Samba
Kirigami.PlaceholderMessage {
property Samba.Installer installer
text: i18nc("@label", "Restart the computer to complete the installation.")
text: i18nc("@label", "Restart the computer to complete the changes.")
helpfulAction: Kirigami.Action {
iconName: "system-restart"
text: i18nc("@button restart the system", "Restart")
onTriggered: installer.reboot()
onTriggered: sambaPlugin.reboot()
}
}
......@@ -31,7 +31,7 @@ Kirigami.ScrollablePage {
changePassword.busy = false
if (userCreated) {
close()
stack.push(pendingStack.pop())
stackReplace(pendingStack.pop())
}
}
......
......@@ -12,13 +12,21 @@ import org.kde.filesharing.samba 1.0 as Samba
QQC2.StackView {
id: stack
Samba.GroupManager {
id: groupManager
}
function stackReplace(target) {
stack.replace(stack.currentItem, target)
}
// The stack of pending pages. Once all backing data is ready we fill the pending stack with all
// pages that ought to get shown eventually. This enables all pages to simply pop the next page and push
// it into the stack once they are done with their thing.
property var pendingStack: []
initialItem: QQC2.BusyIndicator {
running: !sambaPlugin.ready
running: !sambaPlugin.ready || !groupManager.ready
onRunningChanged: {
if (running) {
......@@ -29,6 +37,9 @@ QQC2.StackView {
if (!sambaPlugin.userManager.currentUser().inSamba) {
pendingStack.push("UserPage.qml")
}
if (!groupManager.member) {
pendingStack.push("GroupPage.qml")
}
if (!sambaPlugin.isSambaInstalled()) {
// NB: the plugin may be built without installer support!
if (Samba.Installer === undefined) {
......
......@@ -11,5 +11,6 @@
<file>RebootPage.qml</file>
<file>UserPage.qml</file>
<file>ChangePassword.qml</file>
<file>GroupPage.qml</file>
</qresource>
</RCC>
......@@ -8,7 +8,6 @@
#include "sambainstaller.h"
#include <QDBusInterface>
#include <QDebug>
#include <QFile>
......@@ -42,13 +41,6 @@ void SambaInstaller::install()
});
}
void SambaInstaller::reboot()
{
QDBusInterface interface(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"),
QStringLiteral("org.kde.KSMServerInterface"), QDBusConnection::sessionBus());
interface.asyncCall(QStringLiteral("logout"), 0, 1, 2); // Options: do not ask again | reboot | force
}
bool SambaInstaller::isInstalling() const
{
return m_installing;
......
......@@ -23,7 +23,6 @@ public Q_SLOTS:
bool hasFailed() const;
static bool isInstalled();
static void reboot();
Q_SIGNALS:
void installingChanged();
......
......@@ -20,6 +20,8 @@
#include <QTimer>
#include <QQmlContext>
#include <QPushButton>
#include <QDBusInterface>
#include <QDBusConnection>
#include <KMessageBox>
#include <KPluginFactory>
......@@ -31,6 +33,7 @@
#include "model.h"
#include "usermanager.h"
#include "groupmanager.h"
#ifdef SAMBA_INSTALL
#include "sambainstaller.h"
......@@ -181,6 +184,7 @@ SambaUserSharePlugin::SambaUserSharePlugin(QObject *parent, const QList<QVariant
#ifdef SAMBA_INSTALL
qmlRegisterType<SambaInstaller>("org.kde.filesharing.samba", 1, 0, "Installer");
#endif
qmlRegisterType<GroupManager>("org.kde.filesharing.samba", 1, 0, "GroupManager");
// Need access to the column enum, so register this as uncreatable.
qmlRegisterUncreatableType<UserPermissionModel>("org.kde.filesharing.samba", 1, 0, "UserPermissionModel",
QStringLiteral("Access through sambaPlugin.userPermissionModel"));
......@@ -350,4 +354,11 @@ void SambaUserSharePlugin::setReady(bool ready)
Q_EMIT readyChanged();
}
void SambaUserSharePlugin::reboot()
{
QDBusInterface interface(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"),
QStringLiteral("org.kde.KSMServerInterface"), QDBusConnection::sessionBus());
interface.asyncCall(QStringLiteral("logout"), 0, 1, 2); // Options: do not ask again | reboot | force
}
#include "sambausershareplugin.moc"
......@@ -34,6 +34,7 @@ public:
void applyChanges() override;
Q_INVOKABLE static bool isSambaInstalled();
Q_INVOKABLE static void reboot();
Q_INVOKABLE static void showSambaStatus();
bool isReady() const;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment