Commit 869129c0 authored by Slava Aseev's avatar Slava Aseev Committed by Harald Sitter
Browse files

Add shared folder permissions helper

The helper provides ability to resolve shared folder permissions
on-demand.

If shared folder's permissions (or its paths parts) are
insufficient at some point for some user's ACE the helper suggests
to change permissions and performs required changes after user's
confirmation.

Implementation of permissions resolutions tightly based on:
!16

BUG: 407975
parent 06b22a64
Pipeline #154446 passed with stage
in 41 seconds
......@@ -5,6 +5,7 @@ set(sambausershareplugin_PART_SRCS
model.cpp
usermanager.cpp
groupmanager.cpp
permissionshelper.cpp
qml/qml.qrc
)
......
......@@ -2,6 +2,7 @@
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2011 Rodrigo Belem <rclbelem@gmail.com>
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
*/
#include <kuser.h>
......@@ -162,3 +163,11 @@ QString UserPermissionModel::getAcl() const
return (denials + readables + fulls).join(QLatin1Char(','));
}
UserPermissionModel::SambaACEHashMap UserPermissionModel::getUsersACEs() const {
SambaACEHashMap result;
for (auto it = m_usersAcl.constBegin(); it != m_usersAcl.constEnd(); ++it) {
result.insert(it.key(), it->value<QString>());
}
return result;
}
......@@ -2,6 +2,7 @@
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2011 Rodrigo Belem <rclbelem@gmail.com>
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
*/
#ifndef model_h
......@@ -18,6 +19,8 @@ class UserPermissionModel : public QAbstractTableModel
{
Q_OBJECT
public:
using SambaACEHashMap = QHash<QString, QString>;
enum Column {
ColumnUsername,
ColumnAccess
......@@ -36,6 +39,8 @@ public:
QString getAcl() const;
SambaACEHashMap getUsersACEs() const;
private:
UserManager const *m_userManager = nullptr;
const KSambaShareData m_shareData;
......
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2021 Danil Shein <dshein@altlinux.org>
SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
*/
#include "permissionshelper.h"
#include <KFileItem>
#include <KIO/StatJob>
#include <KUser>
#include <QDebug>
#include <QFileInfo>
#include <QMetaEnum>
#include "model.h"
#include "usermanager.h"
static QString getUserPrimaryGroup(const QString &user)
{
const QStringList groups = KUser(user).groupNames();
if (!groups.isEmpty()) {
return groups.at(0);
}
// if we can't fetch the user's groups then assume the group name is the same as the user name
return user;
}
static KFileItem getCompleteFileItem(const QString &path)
{
const QUrl url = QUrl::fromLocalFile(path);
auto job = KIO::stat(url);
job->exec();
KIO::UDSEntry entry = job->statResult();
KFileItem item(entry, url);
return item;
}
static QString permissionsToString(QFile::Permissions perm)
{
const char permStr[] = {(perm & QFileDevice::ReadOwner) ? 'r' : '-',
(perm & QFileDevice::WriteOwner) ? 'w' : '-',
(perm & QFileDevice::ExeOwner) ? 'x' : '-',
(perm & QFileDevice::ReadGroup) ? 'r' : '-',
(perm & QFileDevice::WriteGroup) ? 'w' : '-',
(perm & QFileDevice::ExeGroup) ? 'x' : '-',
(perm & QFileDevice::ReadOther) ? 'r' : '-',
(perm & QFileDevice::WriteOther) ? 'w' : '-',
(perm & QFileDevice::ExeOther) ? 'x' : '-'};
const int permsAsNum = ((perm & QFileDevice::ReadOwner) ? S_IRUSR : 0)
+ ((perm & QFileDevice::WriteOwner) ? S_IWUSR : 0)
+ ((perm & QFileDevice::ExeOwner) ? S_IXUSR : 0)
+ ((perm & QFileDevice::ReadGroup) ? S_IRGRP : 0)
+ ((perm & QFileDevice::WriteGroup) ? S_IWGRP : 0)
+ ((perm & QFileDevice::ExeGroup) ? S_IXGRP : 0)
+ ((perm & QFileDevice::ReadOther) ? S_IROTH : 0)
+ ((perm & QFileDevice::WriteOther) ? S_IWOTH : 0)
+ ((perm & QFileDevice::ExeOther) ? S_IXOTH : 0);
return QString::fromLatin1(permStr, sizeof(permStr)) + QStringLiteral(" (0%1)").arg(QString::number(permsAsNum, 8));
}
PermissionsHelperModel::PermissionsHelperModel(PermissionsHelper *helper)
: QAbstractTableModel(helper)
, parent(helper)
{
}
int PermissionsHelperModel::rowCount(const QModelIndex &) const
{
return parent->affectedPaths().count();
}
int PermissionsHelperModel::columnCount(const QModelIndex &) const
{
return QMetaEnum::fromType<Column>().keyCount();
}
QVariant PermissionsHelperModel::data(const QModelIndex &index, int role) const
{
if (role == Qt::DisplayRole) {
switch (index.column()) {
case ColumnPath:
return parent->affectedPaths().at(index.row()).path;
case ColumnOldPermissions:
return QVariant::fromValue(permissionsToString(parent->affectedPaths().at(index.row()).oldPerm));
case ColumnNewPermissions:
return QVariant::fromValue(permissionsToString(parent->affectedPaths().at(index.row()).newPerm));
};
}
return {};
}
Qt::ItemFlags PermissionsHelperModel::flags(const QModelIndex &) const
{
return Qt::NoItemFlags;
}
bool PermissionsHelperModel::setData(const QModelIndex &, const QVariant &, int)
{
return false;
}
void PermissionsHelper::addPath(const QFileInfo &fileInfo, QFile::Permissions requiredPermissions)
{
auto oldPerm = fileInfo.permissions();
auto newPerm = oldPerm | requiredPermissions;
m_affectedPaths.append({fileInfo.filePath(), oldPerm, newPerm});
}
PermissionsHelper::PermissionsHelper(const QString &path, const UserManager *userManager, const UserPermissionModel *permissionModel, QObject *parent)
: QObject(parent)
, m_path(path)
, m_userManager(userManager)
, m_permissionModel(permissionModel)
, m_model(new PermissionsHelperModel(this))
{
}
Q_INVOKABLE void PermissionsHelper::reload() {
if (!m_userManager->currentUser()) {
qWarning() << "PermissionsHelper::reload() failed: current user is null";
return;
}
m_affectedPaths.clear();
m_filesWithPosixACL.clear();
QString user = m_userManager->currentUser()->name();
QFile::Permissions permsForShare;
QFile::Permissions permsForSharePath;
auto usersACEs = m_permissionModel->getUsersACEs();
for (auto it = usersACEs.constBegin(); it != usersACEs.constEnd(); ++it) {
const auto &aceUser = it.key();
const auto &access = it.value();
if (aceUser != user) {
if (getUserPrimaryGroup(aceUser) == getUserPrimaryGroup(user)) {
if (access == QLatin1String("R")) {
permsForShare |= (QFile::ExeGroup | QFile::ReadGroup);
} else if (access == QLatin1String("F")) {
permsForShare |= (QFile::ExeGroup | QFile::ReadGroup | QFile::WriteGroup);
}
permsForSharePath |= QFile::ExeGroup;
} else {
if (access == QLatin1String("R")) {
permsForShare |= (QFile::ExeOther | QFile::ReadOther);
} else if (access == QLatin1String("F")) {
permsForShare |= (QFile::ExeOther | QFile::ReadOther | QFile::WriteOther);
}
permsForSharePath |= QFile::ExeOther;
}
}
}
// store share path if permissions are insufficient
QFileInfo fileInfo(m_path);
if (!fileInfo.permission(permsForShare)) {
addPath(fileInfo, permsForShare);
}
// check and store share POSIX ACL
if (getCompleteFileItem(m_path).hasExtendedACL()) {
m_filesWithPosixACL.append(m_path);
}
// check if share path could be resolved (has 'g+x' or 'o+x' all the way through)
if (permsForShare) {
QStringList pathParts = m_path.split(QStringLiteral("/"), Qt::SkipEmptyParts);
pathParts.removeLast();
QString currentPath;
for (const auto &it : qAsConst(pathParts)) {
currentPath.append(QStringLiteral("/") + it);
fileInfo = QFileInfo(currentPath);
if (!fileInfo.permission(permsForSharePath)) {
addPath(fileInfo, permsForSharePath);
}
// check and store share path element's POSIX ACL
if (getCompleteFileItem(currentPath).hasExtendedACL()) {
m_filesWithPosixACL.append(currentPath);
}
}
}
Q_EMIT permissionsChanged();
}
const QList<PermissionsHelper::PermissionsChangeInfo> &PermissionsHelper::affectedPaths() const
{
return m_affectedPaths;
}
Q_INVOKABLE QStringList PermissionsHelper::changePermissions()
{
QStringList failedPaths;
for (const auto &affected : m_affectedPaths) {
// do not break the loop to collecting all possible failed paths
if (!QFile::setPermissions(affected.path, affected.newPerm)) {
failedPaths += affected.path;
}
}
// roll back files permissions if some paths failed
if (!failedPaths.isEmpty()) {
for (const auto &affected : m_affectedPaths) {
if (!QFile::setPermissions(affected.path, affected.oldPerm)) {
qWarning() << "SharePermissionsHelper::sharePermsChange: failed to restore permissions for " << affected.path;
}
}
} else {
m_affectedPaths.clear();
Q_EMIT permissionsChanged();
}
return failedPaths;
}
bool PermissionsHelper::permissionsChangeRequired() const
{
return !m_affectedPaths.empty();
}
bool PermissionsHelper::hasPosixACL() const
{
return !m_filesWithPosixACL.empty();
}
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
*/
#pragma once
#include <QAbstractTableModel>
#include <QFile>
#include <QObject>
#include <QQmlListProperty>
#include <QVariant>
class QFileInfo;
class PermissionsHelper;
class UserPermissionModel;
class UserManager;
class PermissionsHelperModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum Column { ColumnPath, ColumnOldPermissions, ColumnNewPermissions };
Q_ENUM(Column)
explicit PermissionsHelperModel(PermissionsHelper *helper);
int rowCount(const QModelIndex &parent = {}) const override;
int columnCount(const QModelIndex &parent = {}) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
private:
const PermissionsHelper *parent;
};
class PermissionsHelper : public QObject
{
Q_OBJECT
Q_PROPERTY(bool permissionsChangeRequired READ permissionsChangeRequired NOTIFY permissionsChanged)
Q_PROPERTY(bool hasPosixACL READ hasPosixACL NOTIFY permissionsChanged)
Q_PROPERTY(QStringList pathsWithPosixACL MEMBER m_filesWithPosixACL NOTIFY permissionsChanged)
Q_PROPERTY(PermissionsHelperModel *model MEMBER m_model CONSTANT)
public:
struct PermissionsChangeInfo {
QString path;
QFile::Permissions oldPerm;
QFile::Permissions newPerm;
};
explicit PermissionsHelper(const QString &path, const UserManager *userManager, const UserPermissionModel *permissionModel, QObject *parent = nullptr);
const QList<PermissionsChangeInfo> &affectedPaths() const;
bool permissionsChangeRequired() const;
bool hasPosixACL() const;
Q_INVOKABLE QStringList changePermissions();
Q_INVOKABLE void reload();
Q_SIGNALS:
void permissionsChanged();
private:
void addPath(const QFileInfo &fileInfo, QFile::Permissions requiredPermissions);
private:
const QString m_path;
const UserManager *m_userManager;
const UserPermissionModel *m_permissionModel;
PermissionsHelperModel *m_model = nullptr;
QList<PermissionsChangeInfo> m_affectedPaths;
QStringList m_filesWithPosixACL;
};
/*
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>
SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
*/
import QtQuick 2.12
......@@ -51,6 +52,32 @@ when the Share access rules would allow it.`)
ColumnLayout {
anchors.fill: parent
Kirigami.InlineMessage {
id: changePermissionsWarning
Layout.fillWidth: true
showCloseButton: true
visible: sambaPlugin.permissionsHelper.permissionsChangeRequired
type: Kirigami.MessageType.Warning
text: i18nc("@label", "This folder needs extra permissions for sharing to work")
actions: [
Kirigami.Action {
text: i18nc("@action:button opens the change permissions page", "Fix Permissions")
onTriggered: stack.push("ChangePermissionsPage.qml")
}
]
}
Kirigami.InlineMessage {
id: posixACLWarning
Layout.fillWidth: true
showCloseButton: true
visible: sambaPlugin.permissionsHelper.hasPosixACL
type: Kirigami.MessageType.Warning
text: xi18nc("@label", "The share might not work properly because share folder or its paths has Advanced Permissions: %1",
sambaPlugin.permissionsHelper.pathsWithPosixACL.join(", "))
}
QQC2.CheckBox {
id: shareEnabled
text: i18nc("@option:check", "Share this folder with other computers on the local network")
......@@ -218,6 +245,7 @@ when the Share access rules would allow it.`)
denialSheet.maybeOpen()
}
sambaPlugin.dirty = true
sambaPlugin.permissionsHelper.reload()
}
Component.onCompleted: currentIndex = indexOfValue(edit)
}
......
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
*/
import QtQuick 2.12
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
Item {
id: page
ColumnLayout {
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
Kirigami.InlineMessage {
id: changePermissionsError
Layout.fillWidth: true
visible: text !== ""
type: Kirigami.MessageType.Error
}
ColumnLayout {
Layout.fillWidth: true
QQC2.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
text: xi18nc("@info", `
<para>The folder <filename>%1</filename> needs extra permissions for sharing to work.</para>
<para>Do you want to add these permissions now?</para><nl/>
`, sambaPlugin.shareContext.path)
}
Row {
id: row
Layout.fillWidth: true
Repeater {
id: repeater
model: [
i18nc("@title", "File Path"),
i18nc("@title", "Current Permissions"),
i18nc("@title", "Required Permissions")
]
QQC2.Label {
width: row.width / repeater.count
text: modelData
}
}
}
QQC2.ScrollView {
Layout.fillWidth: true
contentItem: TableView {
id: view
property bool itemComplete: false
anchors.fill: parent
clip: true
interactive: false
model: sambaPlugin.permissionsHelper.model
columnWidthProvider: function (column) {
return view.model ? view.width / view.model.columnCount() : 0
}
Timer {
id: forceLayoutTimer
interval: 0
running: false
repeat: false
onTriggered: {
if (view.itemComplete) {
view.forceLayout()
}
}
}
onWidthChanged: forceLayoutTimer.start()
delegate: RowLayout {
Layout.fillWidth: true
QQC2.Label {
Layout.fillWidth: true
text: display
elide: Text.ElideMiddle
}
}
Component.onCompleted: itemComplete = true
}
}
}
Kirigami.ActionToolBar {
alignment: Qt.AlignRight
actions: [
Kirigami.Action {
icon.name: "dialog-ok-apply"
text: i18nc("@action:button changes permissions", "Change Permissions")
onTriggered: {
var failedPaths = sambaPlugin.permissionsHelper.changePermissions()
if (failedPaths.length > 0) {
changePermissionsError.text =
i18nc("@label",
"Could not change permissions for: %1. All permission changes have been reverted to initial state.",
failedPaths.join(", "))
} else {
stack.pop()
}
}
},
Kirigami.Action {
icon.name: "dialog-cancel"
text: i18nc("@action:button cancels permissions change", "Cancel")
onTriggered: stack.pop()
}
]
}
}
}
<!--
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>
SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
-->
<RCC>
<qresource prefix="/org.kde.filesharing.samba/qml/">
......@@ -12,5 +13,6 @@
<file>UserPage.qml</file>
<file>ChangePassword.qml</file>
<file>GroupPage.qml</file>
<file>ChangePermissionsPage.qml</file>
</qresource>
</RCC>
......@@ -4,6 +4,7 @@
SPDX-FileCopyrightText: 2011 Rodrigo Belem <rclbelem@gmail.com>
SPDX-FileCopyrightText: 2015-2020 Harald Sitter <sitter@kde.org>
SPDX-FileCopyrightText: 2019 Nate Graham <nate@kde.org>
SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
*/
#include "sambausershareplugin.h"
......@@ -34,6 +35,7 @@
#include "model.h"
#include "usermanager.h"
#include "groupmanager.h"
#include "permissionshelper.h"
#ifdef SAMBA_INSTALL
#include "sambainstaller.h"
......@@ -49,6 +51,7 @@ class ShareContext : public QObject
Q_PROPERTY(bool guestEnabled READ guestEnabled WRITE setGuestEnabled NOTIFY guestEnabledChanged)
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(int maximumNameLength READ maximumNameLength CONSTANT)
Q_PROPERTY(QString path READ path CONSTANT)
public:
explicit ShareContext(const QUrl &url, QObject *parent = nullptr)
: QObject(parent)
......@@ -98,6 +101,10 @@ public:
return m_shareData.name();
}
QString path() const {
return m_shareData.path();
}
void setName(const QString &name)
{
m_shareData.setName(name);
......@@ -180,6 +187,8 @@ SambaUserSharePlugin::SambaUserSharePlugin(QObject *parent, const QList<QVariant
qmlRegisterAnonymousType<UserManager>("org.kde.filesharing.samba", 1);
qmlRegisterAnonymousType<User>("org.kde.filesharing.samba", 1);
m_model = new UserPermissionModel(m_context->m_shareData, m_userManager, this);
qmlRegisterAnonymousType<PermissionsHelper>("org.kde.filesharing.samba", 1);