diff --git a/samba/filepropertiesplugin/sambausershareplugin.cpp b/samba/filepropertiesplugin/sambausershareplugin.cpp index f7b714065c65a45280f1046df4607f08e2708999..81fe59eed8f1a81ab00f24e1cff33b121dd5060a 100644 --- a/samba/filepropertiesplugin/sambausershareplugin.cpp +++ b/samba/filepropertiesplugin/sambausershareplugin.cpp @@ -4,6 +4,7 @@ SPDX-FileCopyrightText: 2011 Rodrigo Belem SPDX-FileCopyrightText: 2015-2020 Harald Sitter SPDX-FileCopyrightText: 2019 Nate Graham + SPDX-FileCopyrightText: 2021 Danil Shein */ #include "sambausershareplugin.h" @@ -30,6 +31,8 @@ #include #include #include +#include +#include #include "model.h" #include "usermanager.h" @@ -41,6 +44,241 @@ K_PLUGIN_CLASS_WITH_JSON(SambaUserSharePlugin, "sambausershareplugin.json") +class SharePermissionsHelper +{ +public: + explicit SharePermissionsHelper(const QString &path, const QString &acl, const QString &user): + m_acl(acl), + m_user(user), + m_group(getUserPrimaryGroup(user)) + { + sharePermsResolve(path); + } + + QString sharePermsCheck() const + { + QStringList filePermsCheckResult; + + for (auto it = m_sharePerms.constBegin(); it != m_sharePerms.constEnd(); ++it) { + const QString &fname = it.key(); + + filePermsCheckResult += QStringLiteral("File:\t%1\nMode:\t%2 -> %3\n") + .arg(fname) + .arg(prettyPrintFilePerms(it.value().oldPerm)) + .arg(prettyPrintFilePerms(it.value().newPerm)); + } + + return filePermsCheckResult.join(QLatin1Char('\n')); + } + + QString sharePermsChange() const + { + QStringList filePermsChangeResult; + bool rollback = false; + + for (auto it = m_sharePerms.constBegin(); it != m_sharePerms.constEnd(); ++it) { + const QString &fname = it.key(); + const QFile::Permissions perms = it.value().newPerm; + + if (!QFile::setPermissions(fname, perms)) { + // failed to change permissions + filePermsChangeResult += fname; + rollback = true; + } + } + + // roll back files permissions if failed + if (rollback) { + for (auto it = m_sharePerms.constBegin(); it != m_sharePerms.constEnd(); ++it) { + const QString &fname = it.key(); + const QFile::Permissions perms = it.value().oldPerm; + if (!QFile::setPermissions(fname, perms)) { + qWarning("SharePermissionsHelper::sharePermsChange: failed to restore permissions for %s", fname); + } + } + } + + return filePermsChangeResult.join(QLatin1Char('\n')); + } + + bool shareOrPathHasACL() const + { + for (const auto &fileACL : m_sharePosixACL) { + if (fileACL.hasExtendedACL) { + return true; + } + } + return false; + } + + QString sharePermsForElementsWithACL() const + { + QStringList allACLs; + for (auto it = m_sharePosixACL.constBegin(); it != m_sharePosixACL.constEnd(); ++it) { + const QString &fname = it.key(); + if (it.value().hasExtendedACL) { + allACLs += QStringLiteral("File:\t%1\nMode:\t%2\n") + .arg(fname) + .arg(it.value().permsAsString); + } + } + return allACLs.join(QLatin1Char('\n')); + } + +private: + void sharePermsResolve(const QString &path) + { + QFile::Permissions permsForShare; + QFile::Permissions permsForSharePath; + QFile::Permissions oldPerm; + QFile::Permissions newPerm; + QStringList filesWithoutProperPermissions; + + // parse samba acl + const QStringList sambaACLs(m_acl.split(QStringLiteral(","))); + for (const auto &acl : sambaACLs) { + const auto sambaACLRecord = acl.split(QStringLiteral(":")); + const QString &aclUserName = sambaACLRecord.at(0); + const QString &aclAccessMode = sambaACLRecord.at(1); + if (aclUserName != m_user) { + // check if user has same primary group as current user + if (getUserPrimaryGroup(aclUserName) == m_group) { + if (aclAccessMode.endsWith(QStringLiteral("r"))) { + permsForShare |= (QFile::ExeGroup | QFile::ReadGroup); + permsForSharePath |= QFile::ExeGroup; + } else if (aclAccessMode.endsWith(QStringLiteral("f"))) { + permsForShare |= (QFile::ExeGroup | QFile::ReadGroup | QFile::WriteGroup); + permsForSharePath |= QFile::ExeGroup; + } + } else { + if (aclAccessMode.endsWith(QStringLiteral("r"))) { + permsForShare |= (QFile::ExeOther | QFile::ReadOther); + permsForSharePath |= QFile::ExeOther; + } else if (aclAccessMode.endsWith(QStringLiteral("f"))) { + permsForShare |= (QFile::ExeOther | QFile::ReadOther | QFile::WriteOther); + permsForSharePath |= QFile::ExeOther; + } + } + } + } + // store share path if permissions are insufficient + if (!QFileInfo(path).permission(permsForShare)) { + filesWithoutProperPermissions.append(path); + } + // check and store share POSIX ACL + KFileItem currentFileItem = getCompleteFileItem(path); + m_sharePosixACL.insert(path, fileACLsStruct{currentFileItem.hasExtendedACL(), + currentFileItem.permissionsString(), + currentFileItem.ACL().asString(), + currentFileItem.defaultACL().asString()} + ); + // check if share path could be resolved (has 'g+x' or 'o+x' all the way through) + if (permsForShare) { + const QStringList pathParts = path.split(QStringLiteral("/"), Qt::SkipEmptyParts); + QString currentPath; + for (const auto &it : pathParts) { + currentPath.append(QStringLiteral("/%1").arg(it)); + const QFileInfo info(currentPath); + if (!info.permission(permsForSharePath)) { + filesWithoutProperPermissions.append(currentPath); + } + // check and store share path element's POSIX ACL + currentFileItem = getCompleteFileItem(currentPath); + m_sharePosixACL.insert(currentPath, fileACLsStruct{currentFileItem.hasExtendedACL(), + currentFileItem.permissionsString(), + currentFileItem.ACL().asString(), + currentFileItem.defaultACL().asString()} + ); + } + } + + if (!filesWithoutProperPermissions.isEmpty()) { + // we have to change permissions for share and share's path elements + for (const auto ¤tPath : qAsConst(filesWithoutProperPermissions)) { + oldPerm = QFileInfo(currentPath).permissions(); + if (currentPath == path) { + newPerm = oldPerm | permsForShare; + } else { + newPerm = oldPerm | permsForSharePath; + } + // store files and permissions to change later on + m_sharePerms.insert(currentPath, filePermsStruct{oldPerm, newPerm}); + } + } + } + + QString currentUserGroup() const + { + return m_group; + } + + 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; + } + + QString prettyPrintFilePerms(QFile::Permissions perm) const + { + const QString read = QStringLiteral("r"); + const QString write = QStringLiteral("w"); + const QString exe = QStringLiteral("x"); + const QString none = QStringLiteral("-"); + + const QString permsAsRWX = ((perm & QFileDevice::ReadOwner) ? read : none) + + ((perm & QFileDevice::WriteOwner) ? write : none) + + ((perm & QFileDevice::ExeOwner) ? exe : none) + + ((perm & QFileDevice::ReadGroup) ? read : none) + + ((perm & QFileDevice::WriteGroup) ? write : none) + + ((perm & QFileDevice::ExeGroup) ? exe : none) + + ((perm & QFileDevice::ReadOther) ? read : none) + + ((perm & QFileDevice::WriteOther) ? write : none) + + ((perm & QFileDevice::ExeOther) ? exe : none); + + 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 permsAsRWX + QStringLiteral(" (0%1)").arg(QString::number(permsAsNum, 8)); + } + + struct filePermsStruct { + QFile::Permissions oldPerm; + QFile::Permissions newPerm; + }; + struct fileACLsStruct { + bool hasExtendedACL; + QString permsAsString; + QString extACL; + QString defACL; + }; + const QString m_acl; + const QString m_user; + const QString m_group; + QMap m_sharePerms; + QMap m_sharePosixACL; +}; + class ShareContext : public QObject { Q_OBJECT @@ -254,6 +492,85 @@ void SambaUserSharePlugin::applyChanges() return; } + // Check and change permissions for shared folder + const SharePermissionsHelper helper(m_context->m_shareData.path(), + m_model->getAcl(), + m_userManager->currentUser()->name()); + + const QString unmetSharePerms = helper.sharePermsCheck(); + const bool aclFound = helper.shareOrPathHasACL(); + + if (!unmetSharePerms.isEmpty()) { + QString warningMessage; + + if (aclFound) { + warningMessage = xi18nc("@info detailed warning message", + "The folder \"%1\"" + "needs the extra permissions for sharing to work." + "Do you want Dolphin to add these permissions to " + "the folder automatically?" + "Notice: Shared folder or it's path elements has " + "extended Advanced Permissions." + "Network share might not work properly." + "Required permission changes are listed in details " + "below:", + m_context->m_shareData.path()); + } else { + warningMessage = xi18nc("@info detailed warning message", + "The folder \"%1\"" + "needs the extra permissions for sharing to work." + "Do you want Dolphin to add these permissions to " + "the folder automatically?" + "Required permission changes are listed in details " + "below:", + m_context->m_shareData.path()); + } + + const KGuiItem applyButton(i18nc("@button", "Change permissions"), QString(), QString(), QString()); + const KGuiItem cancelButton(i18nc("@button", "Cancel"), QString(), QString(), QString()); + // formatting message details + QString sharePermsDetails = i18n("File permissions changes:\n\n%1").arg(unmetSharePerms); + if (aclFound) { + sharePermsDetails = sharePermsDetails + + i18n("\nFiles with extended Advanced Permissions:\n\n%1") + .arg(helper.sharePermsForElementsWithACL()); + } + + const KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancelDetailed( + properties, + warningMessage, + i18nc("@info warning message caption", + "File permissions changes required"), + applyButton, + cancelButton, + QString(), + KMessageBox::Notify, + sharePermsDetails); + + if (ret == KMessageBox::Continue) { + const QString permsChangeResult = helper.sharePermsChange(); + if (!permsChangeResult.isEmpty()) { + const QString errorMessage = i18nc("@info detailed error message", + "Could not change the permissions for:\n%1\n" + "All permission changes were reverted to initial state.", + permsChangeResult); + KMessageBox::sorry(properties, + errorMessage, + i18nc("@info/title", "Failed to Change Permissions")); + } + } + } else if (aclFound) { + const QString aclMessage = i18nc("@info acl info message", + "Shared folder, or it's path elements\n" + "has extended Advanced Permissions.\n" + "Network share might not work properly.\n\n" + "Files with extended Advanced Permissions:\n\n%1", + helper.sharePermsForElementsWithACL()); + KMessageBox::information(properties, + aclMessage, + i18nc("@info/title", "Advanced Permissions Found")); + } + // TODO: should run this through reportAdd() as well, ACLs may be invalid and then we shouldn't try to save m_context->m_shareData.setAcl(m_model->getAcl()); reportAdd(m_context->m_shareData.save());