File indexing completed on 2025-01-05 04:35:36

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003     SPDX-FileCopyrightText: 2021 Danil Shein <dshein@altlinux.org>
0004     SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
0005     SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0006 */
0007 
0008 #include "permissionshelper.h"
0009 
0010 #include <KFileItem>
0011 #include <KIO/StatJob>
0012 #include <KUser>
0013 #include <QDebug>
0014 #include <QFileInfo>
0015 #include <QMetaEnum>
0016 #include <KLocalizedString>
0017 
0018 #include <QCoro/QCoroSignal>
0019 
0020 #include "model.h"
0021 #include "usermanager.h"
0022 
0023 static QString getUserPrimaryGroup(const QString &user)
0024 {
0025     const QStringList groups = KUser(user).groupNames();
0026     if (!groups.isEmpty()) {
0027         return groups.at(0);
0028     }
0029     // if we can't fetch the user's groups then assume the group name is the same as the user name
0030     return user;
0031 }
0032 
0033 static QCoro::Task<KFileItem> getCompleteFileItem(const QString &path)
0034 {
0035     const QUrl url = QUrl::fromLocalFile(path);
0036     auto job = KIO::stat(url);
0037 
0038     co_await qCoro(job, &KJob::result);
0039 
0040     KIO::UDSEntry entry = job->statResult();
0041     KFileItem item(entry, url);
0042     co_return item;
0043 }
0044 
0045 static QString permissionsToString(QFile::Permissions perm)
0046 {
0047     const char permString[] = {(perm & QFileDevice::ReadOwner) ? 'r' : '-',
0048                                (perm & QFileDevice::WriteOwner) ? 'w' : '-',
0049                                (perm & QFileDevice::ExeOwner) ? 'x' : '-',
0050                                (perm & QFileDevice::ReadGroup) ? 'r' : '-',
0051                                (perm & QFileDevice::WriteGroup) ? 'w' : '-',
0052                                (perm & QFileDevice::ExeGroup) ? 'x' : '-',
0053                                (perm & QFileDevice::ReadOther) ? 'r' : '-',
0054                                (perm & QFileDevice::WriteOther) ? 'w' : '-',
0055                                (perm & QFileDevice::ExeOther) ? 'x' : '-'};
0056 
0057     const int permsAsNumber = ((perm & QFileDevice::ReadOwner) ? S_IRUSR : 0)
0058             + ((perm & QFileDevice::WriteOwner) ? S_IWUSR : 0)
0059             + ((perm & QFileDevice::ExeOwner) ? S_IXUSR : 0)
0060             + ((perm & QFileDevice::ReadGroup) ? S_IRGRP : 0)
0061             + ((perm & QFileDevice::WriteGroup) ? S_IWGRP : 0)
0062             + ((perm & QFileDevice::ExeGroup) ? S_IXGRP : 0)
0063             + ((perm & QFileDevice::ReadOther) ? S_IROTH : 0)
0064             + ((perm & QFileDevice::WriteOther) ? S_IWOTH : 0)
0065             + ((perm & QFileDevice::ExeOther) ? S_IXOTH : 0);
0066 
0067     return QString::fromLatin1(permString, sizeof(permString)) + QStringLiteral(" (0%1)").arg(QString::number(permsAsNumber, 8));
0068 }
0069 
0070 PermissionsHelperModel::PermissionsHelperModel(PermissionsHelper *helper)
0071     : QAbstractTableModel(helper)
0072     , parent(helper)
0073 {
0074 }
0075 
0076 int PermissionsHelperModel::rowCount(const QModelIndex &) const
0077 {
0078     return parent->affectedPaths().count() + 1 /* header */;
0079 }
0080 
0081 int PermissionsHelperModel::columnCount(const QModelIndex &) const
0082 {
0083     return QMetaEnum::fromType<Column>().keyCount();
0084 }
0085 
0086 QVariant PermissionsHelperModel::data(const QModelIndex &index, int role) const
0087 {
0088     if (!index.isValid()) {
0089         return {};
0090     }
0091 
0092     if (index.row() == 0 /* header */) {
0093         switch (index.column()) {
0094         case ColumnPath:
0095             return i18nc("@title", "File Path");
0096         case ColumnOldPermissions:
0097             return i18nc("@title", "Current Permissions");
0098         case ColumnNewPermissions:
0099             return i18nc("@title", "Required Permissions");
0100         };
0101     }
0102 
0103     if (role == Qt::DisplayRole) {
0104         const int row = index.row() - 1 /* header */;
0105         const auto &affectedPath = parent->affectedPaths().at(row);
0106 
0107         switch (index.column()) {
0108         case ColumnPath:
0109             return affectedPath.path;
0110         case ColumnOldPermissions:
0111             return QVariant::fromValue(permissionsToString(affectedPath.oldPerm));
0112         case ColumnNewPermissions:
0113             return QVariant::fromValue(permissionsToString(affectedPath.newPerm));
0114         };
0115     }
0116 
0117     return {};
0118 }
0119 
0120 Qt::ItemFlags PermissionsHelperModel::flags(const QModelIndex &) const
0121 {
0122     return Qt::NoItemFlags;
0123 }
0124 
0125 bool PermissionsHelperModel::setData(const QModelIndex &, const QVariant &, int)
0126 {
0127     return false;
0128 }
0129 
0130 void PermissionsHelper::addPath(const QFileInfo &fileInfo, QFile::Permissions requiredPermissions)
0131 {
0132     auto oldPerm = fileInfo.permissions();
0133     auto newPerm = oldPerm | requiredPermissions;
0134     m_affectedPaths.append({fileInfo.filePath(), oldPerm, newPerm});
0135 }
0136 
0137 PermissionsHelper::PermissionsHelper(const QString &path, const UserManager *userManager, const UserPermissionModel *permissionModel, QObject *parent)
0138     : QObject(parent)
0139     , m_path(path)
0140     , m_userManager(userManager)
0141     , m_permissionModel(permissionModel)
0142     , m_model(new PermissionsHelperModel(this))
0143 {
0144 }
0145 
0146 void PermissionsHelper::reload() {
0147     reloadInternal();
0148 }
0149 
0150 QCoro::Task<void> PermissionsHelper::reloadInternal() {
0151     if (!m_userManager->currentUser()) {
0152         qWarning() << "PermissionsHelper::reload() failed: current user is null";
0153         co_return;
0154     }
0155 
0156     m_affectedPaths.clear();
0157     m_filesWithPosixACL.clear();
0158 
0159     QString user = m_userManager->currentUser()->name();
0160 
0161     QFile::Permissions permsForShare;
0162     QFile::Permissions permsForSharePath;
0163 
0164     auto usersACEs = m_permissionModel->getUsersACEs();
0165     for (auto it = usersACEs.constBegin(); it != usersACEs.constEnd(); ++it) {
0166         const auto &aceUser = it.key();
0167         const auto &access = it.value();
0168 
0169         if (aceUser != user) {
0170             if (getUserPrimaryGroup(aceUser) == getUserPrimaryGroup(user)) {
0171                 if (access == QLatin1String("R")) {
0172                     permsForShare |= (QFile::ExeGroup | QFile::ReadGroup);
0173                 } else if (access == QLatin1String("F")) {
0174                     permsForShare |= (QFile::ExeGroup | QFile::ReadGroup | QFile::WriteGroup);
0175                 }
0176                 permsForSharePath |= QFile::ExeGroup;
0177             } else {
0178                 if (access == QLatin1String("R")) {
0179                     permsForShare |= (QFile::ExeOther | QFile::ReadOther);
0180                 } else if (access == QLatin1String("F")) {
0181                     permsForShare |= (QFile::ExeOther | QFile::ReadOther | QFile::WriteOther);
0182                 }
0183                 permsForSharePath |= QFile::ExeOther;
0184             }
0185         }
0186     }
0187 
0188     // store share path if permissions are insufficient
0189     QFileInfo fileInfo(m_path);
0190     if (!fileInfo.permission(permsForShare)) {
0191         addPath(fileInfo, permsForShare);
0192     }
0193     // check and store share POSIX ACL
0194     KFileItem fileItem = co_await getCompleteFileItem(m_path);
0195 
0196     if (fileItem.hasExtendedACL()) {
0197         m_filesWithPosixACL.append(m_path);
0198     }
0199 
0200     // check if share path could be resolved (has 'g+x' or 'o+x' all the way through)
0201     if (permsForShare) {
0202         QStringList pathParts = m_path.split(QStringLiteral("/"), Qt::SkipEmptyParts);
0203         if (pathParts.count() > 1) {
0204             pathParts.removeLast();
0205             QString currentPath;
0206 
0207             for (const auto &it : qAsConst(pathParts)) {
0208                 currentPath.append(QStringLiteral("/") + it);
0209                 fileInfo = QFileInfo(currentPath);
0210                 if (!fileInfo.permission(permsForSharePath)) {
0211                     addPath(fileInfo, permsForSharePath);
0212                 }
0213                 // check and store share path element's POSIX ACL
0214                 KFileItem fileItem = co_await getCompleteFileItem(m_path);
0215                 if (fileItem.hasExtendedACL()) {
0216                     m_filesWithPosixACL.append(currentPath);
0217                 }
0218             }
0219         }
0220     }
0221 
0222     Q_EMIT permissionsChanged();
0223 }
0224 
0225 const QList<PermissionsHelper::PermissionsChangeInfo> &PermissionsHelper::affectedPaths() const
0226 {
0227     return m_affectedPaths;
0228 }
0229 
0230 Q_INVOKABLE QStringList PermissionsHelper::changePermissions()
0231 {
0232     QStringList failedPaths;
0233 
0234     for (const auto &affected : m_affectedPaths) {
0235         // do not break the loop to collecting all possible failed paths
0236         if (!QFile::setPermissions(affected.path, affected.newPerm)) {
0237             failedPaths += affected.path;
0238         }
0239     }
0240 
0241     // roll back files permissions if some paths failed
0242     if (!failedPaths.isEmpty()) {
0243         for (const auto &affected : m_affectedPaths) {
0244             if (!QFile::setPermissions(affected.path, affected.oldPerm)) {
0245                 qWarning() << "SharePermissionsHelper::sharePermsChange: failed to restore permissions for " << affected.path;
0246             }
0247         }
0248     } else {
0249         m_affectedPaths.clear();
0250         Q_EMIT permissionsChanged();
0251     }
0252 
0253     return failedPaths;
0254 }
0255 
0256 bool PermissionsHelper::permissionsChangeRequired() const
0257 {
0258     return !m_affectedPaths.empty();
0259 }
0260 
0261 bool PermissionsHelper::hasPosixACL() const
0262 {
0263     return !m_filesWithPosixACL.empty();
0264 }