diff --git a/src/core/global.h b/src/core/global.h --- a/src/core/global.h +++ b/src/core/global.h @@ -262,6 +262,16 @@ }; /** + * Specifies privilege file operation status. + * @since 5.43 + */ +enum PrivilegeOperationStatus { + OperationAllowed = 1, + OperationCanceled, + OperationNotAllowed +}; + +/** * Parses the string representation of the cache control option. * * @param cacheControl the string representation diff --git a/src/core/job.cpp b/src/core/job.cpp --- a/src/core/job.cpp +++ b/src/core/job.cpp @@ -33,6 +33,7 @@ #include #include "slave.h" #include "scheduler.h" +#include "slavebase.h" using namespace KIO; @@ -262,6 +263,99 @@ return d_func()->m_outgoingMetaData; } +PrivilegeOperationStatus JobPrivate::tryAskPrivilegeOpConfirmation() +{ + if (m_confirmationAsked) { + return OperationAllowed; + } + + if (m_parentJob) { + if (!m_parentJob->d_func()->m_privilegeExecutionEnabled) { + return OperationNotAllowed; + } + + if (!m_parentJob->d_func()->m_confirmationAsked) { + PrivilegeOperationStatus opStatus = m_parentJob->d_func()->tryAskPrivilegeOpConfirmation(); + if (opStatus == OperationAllowed) { + // Copy meta-data from parent job + m_incomingMetaData.insert(QStringLiteral("TestData"), m_parentJob->queryMetaData(QStringLiteral("TestData"))); + m_confirmationAsked = true; + } + return opStatus; + } else { + return OperationAllowed; + } + } else { + // In case of SimpleJob like chmod, chown, etc. which don't accept JobFlags + if (!m_privilegeExecutionEnabled) { + return OperationNotAllowed; + } + } + + switch (m_operationType) { + case ChangeAttr: + m_caption = i18n("Change Attribute"); + m_message = i18n("Root privileges are required to change file attributes. " + "Do you want to continue?"); + break; + case Copy: + m_caption = i18n("Copy Files"); + m_message = i18n("Root privileges are required to complete the copy operation. " + "Do you want to continue?"); + break; + case Delete: + m_caption = i18n("Delete Files"); + m_message = i18n("Root privileges are required to complete the delete operation." + "However, doing so may damage your system. Do you want to continue?"); + break; + case MkDir: + m_caption = i18n("Create Folder"); + m_message = i18n("Root privileges are required to create this folder. " + "Do you want to continue?"); + break; + case Move: + m_caption = i18n("Move Items"); + m_message = i18n("Root privileges are required to complete the move operation. " + "Do you want to continue?"); + break; + case Rename: + m_caption = i18n("Rename"); + m_message = i18n("Root privileges are required to complete renaming. " + "Do you want to continue?"); + break; + case Symlink: + m_caption = i18n("Create Symlink"); + m_message = i18n("Root privileges are required to create a symlink. " + "Do you want to continue?"); + break; + case Transfer: + m_caption = i18n("Transfer data"); + m_message = i18n("Root privileges are required to complete transferring data. " + "Do you want to continue?"); + default: + break; + } + + if (m_outgoingMetaData.value(QStringLiteral("UnitTesting")) == QLatin1String("true")) { + // Set meta-data for the top-level job + m_incomingMetaData.insert(QStringLiteral("TestData"), QStringLiteral("PrivilegeOperationAllowed")); + return OperationAllowed; + } + + if (!m_uiDelegateExtension) { + return OperationNotAllowed; + } + + int status = m_uiDelegateExtension->requestMessageBox(JobUiDelegateExtension::WarningContinueCancel, + m_message, m_caption, i18n("Continue"), i18n("Cancel")); + m_confirmationAsked = true; + + if (status == SlaveBase::Cancel) { + return OperationCanceled; + } + return OperationAllowed; +} + ////////////////////////// class KIO::DirectCopyJobPrivate: public KIO::SimpleJobPrivate diff --git a/src/core/job_base.h b/src/core/job_base.h --- a/src/core/job_base.h +++ b/src/core/job_base.h @@ -301,7 +301,26 @@ * This is used by KIO::rename(), KIO::put(), KIO::file_copy(), KIO::file_move(), KIO::symlink(). * Otherwise the operation will fail with ERR_FILE_ALREADY_EXIST or ERR_DIR_ALREADY_EXIST. */ - Overwrite = 4 + Overwrite = 4, + + //TODO: Only one of these flags is required. Remove the one less stable. + /** + * When set, notifies the slave that application/job does not want privilege execution. + * So in case of failure due to insufficient privileges show an error without attempting + * to run the operation as root first. + * + * @since 5.43 + */ + NoPrivilegeExecution = 8, + + /** + * When set, notifies the slave that the application(job) wants the file operation to be + * performed as root user if there occurs a failure due to insufficient privilege. + * + * @since 5.43 + */ + PrivilegeExecution = 16 + }; Q_DECLARE_FLAGS(JobFlags, JobFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(JobFlags) diff --git a/src/core/job_p.h b/src/core/job_p.h --- a/src/core/job_p.h +++ b/src/core/job_p.h @@ -34,6 +34,7 @@ #include #include #include "kiocoredebug.h" +#include "global.h" #define KIO_ARGS QByteArray packedArgs; QDataStream stream( &packedArgs, QIODevice::WriteOnly ); stream @@ -47,7 +48,8 @@ public: JobPrivate() : m_parentJob(nullptr), m_extraFlags(0), - m_uiDelegateExtension(KIO::defaultJobUiDelegateExtension()) + m_uiDelegateExtension(KIO::defaultJobUiDelegateExtension()), + m_privilegeExecutionEnabled(false), m_confirmationAsked(false) { } @@ -64,6 +66,18 @@ EF_KillCalled = (1 << 4) }; + enum FileOperationType { + ChangeAttr, // chmod(), chown(), setModificationTime() + Copy, + Delete, + MkDir, + Move, + Rename, + Symlink, + Transfer, // put() and get() + Other // if other file operation set message, caption inside the job. + }; + // Maybe we could use the QObject parent/child mechanism instead // (requires a new ctor, and moving the ctor code to some init()). Job *m_parentJob; @@ -73,7 +87,13 @@ MetaData m_outgoingMetaData; JobUiDelegateExtension *m_uiDelegateExtension; Job *q_ptr; + // For privilege operation + bool m_privilegeExecutionEnabled; + bool m_confirmationAsked; + QString m_caption, m_message; + FileOperationType m_operationType; + PrivilegeOperationStatus tryAskPrivilegeOpConfirmation(); void slotSpeed(KJob *job, unsigned long speed); static void emitMoving(KIO::Job *, const QUrl &src, const QUrl &dest); @@ -173,6 +193,11 @@ void _k_slotSlaveInfoMessage(const QString &s); /** + * Called when privilegeOperationRequested() is emitted by slave. + */ + void slotPrivilegeOperationRequested(); + + /** * @internal * Called by the scheduler when a slave gets to * work on this job. diff --git a/src/core/simplejob.h b/src/core/simplejob.h --- a/src/core/simplejob.h +++ b/src/core/simplejob.h @@ -146,6 +146,7 @@ Q_PRIVATE_SLOT(d_func(), void slotSpeed(unsigned long speed)) Q_PRIVATE_SLOT(d_func(), void slotTotalSize(KIO::filesize_t data_size)) Q_PRIVATE_SLOT(d_func(), void _k_slotSlaveInfoMessage(const QString &)) + Q_PRIVATE_SLOT(d_func(), void slotPrivilegeOperationRequested()) Q_DECLARE_PRIVATE(SimpleJob) }; diff --git a/src/core/simplejob.cpp b/src/core/simplejob.cpp --- a/src/core/simplejob.cpp +++ b/src/core/simplejob.cpp @@ -148,6 +148,9 @@ q->connect(slave, SIGNAL(finished()), SLOT(slotFinished())); + q->connect(slave, SIGNAL(privilegeOperationRequested()), + SLOT(slotPrivilegeOperationRequested())); + if ((m_extraFlags & EF_TransferJobDataSent) == 0) { // this is a "get" job q->connect(slave, SIGNAL(totalSize(KIO::filesize_t)), SLOT(slotTotalSize(KIO::filesize_t))); @@ -337,6 +340,11 @@ Q_UNUSED(redirectionURL); } +void SimpleJobPrivate::slotPrivilegeOperationRequested() +{ + m_slave->send(MSG_PRIVILEGE_EXEC, QByteArray::number(tryAskPrivilegeOpConfirmation())); +} + ////////// SimpleJob *KIO::rmdir(const QUrl &url) { diff --git a/src/core/slavebase.h b/src/core/slavebase.h --- a/src/core/slavebase.h +++ b/src/core/slavebase.h @@ -935,6 +935,14 @@ */ int waitForHostInfo(QHostInfo &info); + /** + * Checks with job if privilege operation is allowed. + * @return privilege operation status. + * @see PrivilegeOperationStatus + * @since 5.43 + */ + PrivilegeOperationStatus requestPrivilegeOperation(); + protected: /** * Name of the protocol supported by this slave diff --git a/src/core/slavebase.cpp b/src/core/slavebase.cpp --- a/src/core/slavebase.cpp +++ b/src/core/slavebase.cpp @@ -1457,3 +1457,11 @@ return result; } + +PrivilegeOperationStatus SlaveBase::requestPrivilegeOperation() +{ + QByteArray buffer; + send(MSG_PRIVILEGE_EXEC); + waitForAnswer(MSG_PRIVILEGE_EXEC, 0, buffer); + return KIO::PrivilegeOperationStatus(buffer.toInt()); +} diff --git a/src/core/slaveinterface.h b/src/core/slaveinterface.h --- a/src/core/slaveinterface.h +++ b/src/core/slaveinterface.h @@ -84,7 +84,8 @@ MSG_DEL_AUTH_KEY, ///< @deprecated MSG_OPENED, MSG_WRITTEN, - MSG_HOST_INFO_REQ + MSG_HOST_INFO_REQ, + MSG_PRIVILEGE_EXEC // add new ones here once a release is done, to avoid breaking binary compatibility }; @@ -144,6 +145,8 @@ void open(); void written(KIO::filesize_t); + void privilegeOperationRequested(); + /////////// // Info sent by the slave ////////// diff --git a/src/core/slaveinterface.cpp b/src/core/slaveinterface.cpp --- a/src/core/slaveinterface.cpp +++ b/src/core/slaveinterface.cpp @@ -319,6 +319,9 @@ HostInfo::lookupHost(hostName, this, SLOT(slotHostInfo(QHostInfo))); break; } + case MSG_PRIVILEGE_EXEC: + emit privilegeOperationRequested(); + break; default: qCWarning(KIO_CORE) << "Slave sends unknown command (" << _cmd << "), dropping slave"; return false; diff --git a/src/ioslaves/file/CMakeLists.txt b/src/ioslaves/file/CMakeLists.txt --- a/src/ioslaves/file/CMakeLists.txt +++ b/src/ioslaves/file/CMakeLists.txt @@ -8,7 +8,7 @@ if(WIN32) set(kio_file_PART_SRCS file.cpp file_win.cpp ) else() - set(kio_file_PART_SRCS file.cpp file_unix.cpp ) + set(kio_file_PART_SRCS file.cpp file_unix.cpp fdreceiver.cpp ) endif() find_package(ACL) @@ -23,6 +23,10 @@ add_library(kio_file MODULE ${kio_file_PART_SRCS}) target_link_libraries(kio_file KF5::KIOCore KF5::I18n) +if(UNIX) + target_link_libraries(kio_file Qt5::Network KF5::Auth) +endif() + if (HAVE_VOLMGT AND CMAKE_SYSTEM_NAME MATCHES SunOS) target_link_libraries(kio_file -lvolmgt) endif () @@ -34,3 +38,7 @@ set_target_properties(kio_file PROPERTIES OUTPUT_NAME "file") set_target_properties(kio_file PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/kf5/kio") install(TARGETS kio_file DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/kio) + +if (UNIX) + add_subdirectory(kauth) +endif() diff --git a/src/ioslaves/file/fdreceiver.h b/src/ioslaves/file/fdreceiver.h new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/fdreceiver.h @@ -0,0 +1,47 @@ +/*** + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +***/ + +#ifndef FDRECEIVER_H +#define FDRECEIVER_H + +#include +#include + +class QSocketNotifier; +class FdReceiver : public QObject +{ + Q_OBJECT + +public: + FdReceiver(const QString &path, QObject *parent = nullptr); + ~FdReceiver(); + + bool isListening() const; + int fileDescriptor() const; + +private: + Q_SLOT void receiveFileDescriptor(); + + QSocketNotifier *m_readNotifier; + int m_socketDes; + int m_fileDes; +}; + +#endif diff --git a/src/ioslaves/file/fdreceiver.cpp b/src/ioslaves/file/fdreceiver.cpp new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/fdreceiver.cpp @@ -0,0 +1,80 @@ +/*** + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +***/ + +#include +#include + +#include + +#include "sharefd_p.h" +#include "fdreceiver.h" + +FdReceiver::FdReceiver(const QString &path, QObject *parent) + : QObject(parent) + , m_readNotifier(nullptr) + , m_socketDes(-1) + , m_fileDes(-1) +{ + m_socketDes = ::socket(AF_LOCAL, SOCK_STREAM|SOCK_NONBLOCK, 0); + if (m_socketDes == -1) { + std::cerr << "socket error:" << strerror(errno) << std::endl; + return; + } + + const SocketAddress addr(path.toStdString()); + if (bind(m_socketDes, addr.address(), addr.length()) != 0 || listen(m_socketDes, 5) != 0) { + std::cerr << "bind/listen error:" << strerror(errno) << std::endl; + ::close(m_socketDes); + m_socketDes = -1; + return; + } + + m_readNotifier = new QSocketNotifier(m_socketDes, QSocketNotifier::Read, this); + connect(m_readNotifier, &QSocketNotifier::activated, this, &FdReceiver::receiveFileDescriptor); +} + +FdReceiver::~FdReceiver() +{ + if (m_socketDes >= 0) { + ::close(m_socketDes); + } +} + +bool FdReceiver::isListening() const +{ + return m_socketDes >= 0 && m_readNotifier; +} + +void FdReceiver::receiveFileDescriptor() +{ + int client = ::accept(m_socketDes, NULL, NULL); + if (client > 0) { + FDMessageHeader msg; + if (::recvmsg(client, msg.message(), 0) == 2) { + ::memcpy(&m_fileDes, CMSG_DATA(msg.cmsgHeader()), sizeof m_fileDes); + } + ::close(client); + } +} + +int FdReceiver::fileDescriptor() const +{ + return m_fileDes; +} diff --git a/src/ioslaves/file/file.h b/src/ioslaves/file/file.h --- a/src/ioslaves/file/file.h +++ b/src/ioslaves/file/file.h @@ -39,6 +39,8 @@ #include #endif +#include "file_p.h" + #include Q_DECLARE_LOGGING_CATEGORY(KIO_FILE) @@ -100,6 +102,17 @@ void fileSystemFreeSpace(const QUrl &url); // KF6 TODO: Turn into virtual method in SlaveBase + bool privilegeOperationUnitTestMode(); + PrivilegeOperationReturnValue execWithElevatedPrivilege(ActionType action, const QVariant &arg1, + const QVariant &arg2 = QVariant(), + const QVariant &arg3 = QVariant()); + PrivilegeOperationReturnValue tryOpen(QFile &f, const QByteArray &path, int flags, int mode); + + // We want to execute chmod/chown/utime with elevated privileges (in copy & put) + // only during the brief period privileges are elevated. If it's not the case show + // a warning and continue. + PrivilegeOperationReturnValue tryChangeFileAttr(ActionType action, const QVariant &arg1, + const QVariant &arg2, const QVariant &arg3 = QVariant()); private: mutable QHash mUsercache; mutable QHash mGroupcache; diff --git a/src/ioslaves/file/file.cpp b/src/ioslaves/file/file.cpp --- a/src/ioslaves/file/file.cpp +++ b/src/ioslaves/file/file.cpp @@ -77,6 +77,8 @@ #include #include +#include "fdreceiver.h" + Q_LOGGING_CATEGORY(KIO_FILE, "kf5.kio.kio_file") // Pseudo plugin class to embed meta data @@ -234,26 +236,30 @@ (setACL(_path.data(), permissions, false) == -1) || /* if not a directory, cannot set default ACLs */ (setACL(_path.data(), permissions, true) == -1 && errno != ENOTDIR)) { - - switch (errno) { - case EPERM: - case EACCES: - error(KIO::ERR_ACCESS_DENIED, path); - break; -#if defined(ENOTSUP) - case ENOTSUP: // from setACL since chmod can't return ENOTSUP - error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path)); - break; -#endif - case ENOSPC: - error(KIO::ERR_DISK_FULL, path); - break; - default: - error(KIO::ERR_CANNOT_CHMOD, path); + if (auto err = execWithElevatedPrivilege(CHMOD, _path, permissions)) { + if (!err.wasCanceled()) { + switch (errno) { + case EPERM: + case EACCES: + error(KIO::ERR_ACCESS_DENIED, path); + break; + #if defined(ENOTSUP) + case ENOTSUP: // from setACL since chmod can't return ENOTSUP + error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path)); + break; + #endif + case ENOSPC: + error(KIO::ERR_DISK_FULL, path); + break; + default: + error(KIO::ERR_CANNOT_CHMOD, path); + } + return; + } } - } else { - finished(); } + + finished(); } void FileProtocol::setModificationTime(const QUrl &url, const QDateTime &mtime) @@ -265,8 +271,12 @@ utbuf.actime = statbuf.st_atime; // access time, unchanged utbuf.modtime = mtime.toTime_t(); // modification time if (::utime(QFile::encodeName(path).constData(), &utbuf) != 0) { - // TODO: errno could be EACCES, EPERM, EROFS - error(KIO::ERR_CANNOT_SETTIME, path); + if (auto err = execWithElevatedPrivilege(UTIME, path, qint64(utbuf.actime), qint64(utbuf.modtime))) { + if (!err.wasCanceled()) { + // TODO: errno could be EACCES, EPERM, EROFS + error(KIO::ERR_CANNOT_SETTIME, path); + } + } } else { finished(); } @@ -283,16 +293,26 @@ // Remove existing file or symlink, if requested (#151851) if (metaData(QStringLiteral("overwrite")) == QLatin1String("true")) { - QFile::remove(path); + if (!QFile::remove(path)) { + execWithElevatedPrivilege(DEL, path); + } } QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(path).constData(), &buff) == -1) { - if (!QDir().mkdir(path)) { - //TODO: add access denied & disk full (or another reasons) handling (into Qt, possibly) - error(KIO::ERR_CANNOT_MKDIR, path); - return; - } else { + bool dirCreated; + if (!(dirCreated = QDir().mkdir(path))) { + if (auto err = execWithElevatedPrivilege(MKDIR, path)) { + if (!err.wasCanceled()) { + //TODO: add access denied & disk full (or another reasons) handling (into Qt, possibly) + error(KIO::ERR_CANNOT_MKDIR, path); + } + return; + } + dirCreated = true; + } + + if (dirCreated) { if (permissions != -1) { chmod(url, permissions); } else { @@ -343,8 +363,12 @@ QFile f(path); if (!f.open(QIODevice::ReadOnly)) { - error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); - return; + if (auto err = tryOpen(f, QFile::encodeName(path), O_RDONLY, S_IRUSR)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); + } + return; + } } #if HAVE_FADVISE @@ -538,6 +562,11 @@ void FileProtocol::put(const QUrl &url, int _mode, KIO::JobFlags _flags) { + if (privilegeOperationUnitTestMode()) { + finished(); + return; + } + const QString dest_orig = url.toLocalFile(); // qDebug() << dest_orig << "mode=" << _mode; @@ -619,15 +648,36 @@ } if (!f.isOpen()) { - // qDebug() << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode; - // qDebug() << "QFile error==" << f.error() << "(" << f.errorString() << ")"; + int oflags = 0; + int filemode = _mode; - if (f.error() == QFileDevice::PermissionsError) { - error(KIO::ERR_WRITE_ACCESS_DENIED, dest); + if ((_flags & KIO::Resume)) { + oflags = O_RDWR | O_APPEND; } else { - error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); + oflags = O_WRONLY | O_TRUNC | O_CREAT; + if (_mode != -1) { + filemode = _mode | S_IWUSR | S_IRUSR; + } + } + + if (auto err = tryOpen(f, QFile::encodeName(dest), oflags, filemode)) { + if (!err.wasCanceled()) { + // qDebug() << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode; + // qDebug() << "QFile error==" << f.error() << "(" << f.errorString() << ")"; + + if (f.error() == QFileDevice::PermissionsError) { + error(KIO::ERR_WRITE_ACCESS_DENIED, dest); + } else { + error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); + } + } + return; + } else { + if ((_flags & KIO::Resume)) { + execWithElevatedPrivilege(CHOWN, dest, getuid(), getgid()); + QFile::setPermissions(dest, modeToQFilePermissions(filemode)); + } } - return; } } @@ -681,23 +731,28 @@ //QFile::rename() never overwrites the destination file unlike ::remove, //so we must remove it manually first if (_flags & KIO::Overwrite) { - QFile::remove(dest_orig); + if (!QFile::remove(dest_orig)) { + execWithElevatedPrivilege(DEL, dest_orig); + } } if (!QFile::rename(dest, dest_orig)) { - qCWarning(KIO_FILE) << " Couldn't rename " << dest << " to " << dest_orig; - error(KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig); - return; + if (auto err = execWithElevatedPrivilege(RENAME, dest, dest_orig)) { + if (!err.wasCanceled()) { + qCWarning(KIO_FILE) << " Couldn't rename " << dest << " to " << dest_orig; + error(KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig); + } + return; + } } org::kde::KDirNotify::emitFileRenamed(QUrl::fromLocalFile(dest), QUrl::fromLocalFile(dest_orig)); } // set final permissions if (_mode != -1 && !(_flags & KIO::Resume)) { if (!QFile::setPermissions(dest_orig, modeToQFilePermissions(_mode))) { // couldn't chmod. Eat the error if the filesystem apparently doesn't support it. - KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest_orig); - if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) { + if (tryChangeFileAttr(CHMOD, dest_orig, _mode)) { warning(i18n("Could not change permissions for\n%1", dest_orig)); } } @@ -723,7 +778,9 @@ struct utimbuf utbuf; utbuf.actime = dest_statbuf.st_atime; utbuf.modtime = dt.toTime_t(); - utime(QFile::encodeName(dest_orig).constData(), &utbuf); + if (utime(QFile::encodeName(dest_orig).constData(), &utbuf) != 0) { + tryChangeFileAttr(UTIME, dest_orig, qint64(utbuf.actime), qint64(utbuf.modtime)); + } #endif } } @@ -1335,17 +1392,25 @@ } else { //qDebug() << "QFile::remove" << itemPath; if (!QFile::remove(itemPath)) { - error(KIO::ERR_CANNOT_DELETE, itemPath); - return false; + if (auto err = execWithElevatedPrivilege(DEL, itemPath)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE, itemPath); + } + return false; + } } } } QDir dir; Q_FOREACH (const QString &itemPath, dirsToDelete) { //qDebug() << "QDir::rmdir" << itemPath; if (!dir.rmdir(itemPath)) { - error(KIO::ERR_CANNOT_DELETE, itemPath); - return false; + if (auto err = execWithElevatedPrivilege(RMDIR, itemPath)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE, itemPath); + } + return false; + } } } return true; diff --git a/src/ioslaves/file/file_p.h b/src/ioslaves/file/file_p.h new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/file_p.h @@ -0,0 +1,66 @@ +/*** + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +***/ + +#ifndef FILE_P_H +#define FILE_P_H + +enum ActionType { + CHMOD = 1, + CHOWN, + DEL, + MKDIR, + OPEN, + OPENDIR, + RENAME, + RMDIR, + SYMLINK, + UTIME, +}; + +/** + * PrivilegeOperationReturnValue encapsulates the return value from execWithElevatedPrivilege() in a convenient way. + * Warning, this class will cast to a bool that is false on success and true on failure. This unusual solution allows + * to write kioslave code like this: + +if (!dir.rmdir(itemPath)) { + if (auto ret = execWithElevatedPrivilege(RMDIR, itemPath)) { + if (!ret.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE, itemPath); + } + return false; + } +} +// directory successfully removed, continue with the next operation +*/ +class PrivilegeOperationReturnValue +{ +public: + static PrivilegeOperationReturnValue success() { return PrivilegeOperationReturnValue{false, false}; } + static PrivilegeOperationReturnValue failure() { return PrivilegeOperationReturnValue{true, false}; } + static PrivilegeOperationReturnValue canceled() { return PrivilegeOperationReturnValue{true, true}; } + operator bool() const { return m_failed; } + bool wasCanceled() const { return m_canceled; } +private: + PrivilegeOperationReturnValue(bool failed, bool canceled) : m_failed(failed), m_canceled(canceled) {} + const bool m_failed; + const bool m_canceled; +}; + +#endif diff --git a/src/ioslaves/file/file_unix.cpp b/src/ioslaves/file/file_unix.cpp --- a/src/ioslaves/file/file_unix.cpp +++ b/src/ioslaves/file/file_unix.cpp @@ -39,6 +39,10 @@ #include #include +#include + +#include "fdreceiver.h" + //sendfile has different semantics in different platforms #if defined HAVE_SENDFILE && defined Q_OS_LINUX #define USE_SENDFILE 1 @@ -63,9 +67,71 @@ return false; } +static const QString socketPath() +{ + return QStringLiteral("org_kde_kio_file_helper_%1").arg(getpid()); +} + +bool FileProtocol::privilegeOperationUnitTestMode() +{ + return (metaData(QStringLiteral("UnitTesting")) == QLatin1String("true")) + && (requestPrivilegeOperation() == KIO::OperationAllowed); +} + +PrivilegeOperationReturnValue FileProtocol::tryOpen(QFile &f, const QByteArray &path, int flags, int mode) +{ + FdReceiver fdRecv(socketPath()); + if (!fdRecv.isListening()) { + return PrivilegeOperationReturnValue::failure(); + } + + QIODevice::OpenMode openMode; + if (flags & O_RDONLY) { + openMode |= QIODevice::ReadOnly; + } + if (flags & O_WRONLY || flags & O_CREAT) { + openMode |= QIODevice::WriteOnly; + } + if (flags & O_RDWR) { + openMode |= QIODevice::ReadWrite; + } + if (flags & O_TRUNC) { + openMode |= QIODevice::Truncate; + } + if (flags & O_APPEND) { + openMode |= QIODevice::Append; + } + + if (auto err = execWithElevatedPrivilege(OPEN, path, flags, mode)) { + return err; + } else { + int fd = fdRecv.fileDescriptor(); + if (fd < 3 || !f.open(fd, openMode, QFileDevice::AutoCloseHandle)) { + return PrivilegeOperationReturnValue::failure(); + } + } + return PrivilegeOperationReturnValue::success(); +} + +PrivilegeOperationReturnValue FileProtocol::tryChangeFileAttr(ActionType action, const QVariant &arg1, + const QVariant &arg2, const QVariant &arg3) +{ + KAuth::Action execAction(QStringLiteral("org.kde.kio.file.exec")); + execAction.setHelperId(QStringLiteral("org.kde.kio.file")); + if (execAction.status() == KAuth::Action::AuthorizedStatus) { + return execWithElevatedPrivilege(action, arg1, arg2, arg3); + } + return PrivilegeOperationReturnValue::failure(); +} + void FileProtocol::copy(const QUrl &srcUrl, const QUrl &destUrl, int _mode, JobFlags _flags) { + if (privilegeOperationUnitTestMode()) { + finished(); + return; + } + // qDebug() << "copy(): " << srcUrl << " -> " << destUrl << ", mode=" << _mode; const QString src = srcUrl.toLocalFile(); @@ -118,34 +184,60 @@ // the symlink actually points to current source! if ((_flags & KIO::Overwrite) && ((buff_dest.st_mode & QT_STAT_MASK) == QT_STAT_LNK)) { //qDebug() << "copy(): LINK DESTINATION"; - QFile::remove(dest); + if (!QFile::remove(dest)) { + if (auto err = execWithElevatedPrivilege(DEL, _dest)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE_ORIGINAL, dest); + } + return; + } + } } } QFile src_file(src); if (!src_file.open(QIODevice::ReadOnly)) { - error(KIO::ERR_CANNOT_OPEN_FOR_READING, src); - return; + if (auto err = tryOpen(src_file, _src, O_RDONLY, S_IRUSR)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_OPEN_FOR_READING, src); + } + return; + } } #if HAVE_FADVISE posix_fadvise(src_file.handle(), 0, 0, POSIX_FADV_SEQUENTIAL); #endif QFile dest_file(dest); if (!dest_file.open(QIODevice::Truncate | QIODevice::WriteOnly)) { - // qDebug() << "###### COULD NOT WRITE " << dest; - if (errno == EACCES) { - error(KIO::ERR_WRITE_ACCESS_DENIED, dest); - } else { - error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); + if (auto err = tryOpen(dest_file, _dest, O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR)) { + if (!err.wasCanceled()) { + // qDebug() << "###### COULD NOT WRITE " << dest; + if (errno == EACCES) { + error(KIO::ERR_WRITE_ACCESS_DENIED, dest); + } else { + error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); + } + } + src_file.close(); + return; } - src_file.close(); - return; } // nobody shall be allowed to peek into the file during creation - dest_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); + if (!QFile::setPermissions(dest, QFileDevice::ReadOwner | QFileDevice::WriteOwner)) { + if (auto err = execWithElevatedPrivilege(CHOWN, _dest, getuid(), getgid())) { + dest_file.close(); + execWithElevatedPrivilege(DEL, _dest); + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_CHOWN, dest); + } + return; + } else { + QFile::setPermissions(dest, QFileDevice::ReadOwner | QFileDevice::WriteOwner); + } + } #if HAVE_FADVISE posix_fadvise(dest_file.handle(), 0, 0, POSIX_FADV_SEQUENTIAL); @@ -206,7 +298,9 @@ acl_free(acl); } #endif - dest_file.remove(); // don't keep partly copied file + if (!QFile::remove(dest)) { // don't keep partly copied file + execWithElevatedPrivilege(DEL, _dest); + } return; } if (n == 0) { @@ -227,7 +321,9 @@ acl_free(acl); } #endif - dest_file.remove(); // don't keep partly copied file + if (!QFile::remove(dest)) { // don't keep partly copied file + execWithElevatedPrivilege(DEL, _dest); + } return; } processed_size += n; @@ -248,7 +344,9 @@ acl_free(acl); } #endif - dest_file.remove(); // don't keep partly copied file + if (!QFile::remove(dest)) { // don't keep partly copied file + execWithElevatedPrivilege(DEL, _dest); + } return; } @@ -263,9 +361,7 @@ || (acl && acl_set_file(_dest.data(), ACL_TYPE_ACCESS, acl) != 0) #endif ) { - KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest); - // Eat the error if the filesystem apparently doesn't support chmod. - if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) { + if (tryChangeFileAttr(CHMOD, _dest, _mode)) { warning(i18n("Could not change permissions for '%1'", dest)); } } @@ -281,15 +377,19 @@ // we might not be allowed to change the owner (void)::chown(_dest.data(), buff_src.st_uid, -1 /*keep group*/); } else { - qCWarning(KIO_FILE) << QStringLiteral("Couldn't preserve group for '%1'").arg(dest); + if (tryChangeFileAttr(CHOWN, _dest, buff_src.st_uid, buff_src.st_gid)) { + qCWarning(KIO_FILE) << QStringLiteral("Couldn't preserve group for '%1'").arg(dest); + } } // copy access and modification time struct utimbuf ut; ut.actime = buff_src.st_atime; ut.modtime = buff_src.st_mtime; if (::utime(_dest.data(), &ut) != 0) { - qCWarning(KIO_FILE) << QStringLiteral("Couldn't preserve access and modification time for '%1'").arg(dest); + if (tryChangeFileAttr(UTIME, _dest, qint64(ut.actime), qint64(ut.modtime))) { + qCWarning(KIO_FILE) << QStringLiteral("Couldn't preserve access and modification time for '%1'").arg(dest); + } } processedSize(buff_src.st_size); @@ -464,16 +564,20 @@ } if (::rename(_src.data(), _dest.data())) { - if ((errno == EACCES) || (errno == EPERM)) { - error(KIO::ERR_ACCESS_DENIED, dest); - } else if (errno == EXDEV) { - error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename")); - } else if (errno == EROFS) { // The file is on a read-only filesystem - error(KIO::ERR_CANNOT_DELETE, src); - } else { - error(KIO::ERR_CANNOT_RENAME, src); + if (auto err = execWithElevatedPrivilege(RENAME, _src, _dest)) { + if (!err.wasCanceled()) { + if ((errno == EACCES) || (errno == EPERM)) { + error(KIO::ERR_ACCESS_DENIED, dest); + } else if (errno == EXDEV) { + error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename")); + } else if (errno == EROFS) { // The file is on a read-only filesystem + error(KIO::ERR_CANNOT_DELETE, src); + } else { + error(KIO::ERR_CANNOT_RENAME, src); + } + } + return; } - return; } finished(); @@ -489,8 +593,12 @@ if ((flags & KIO::Overwrite)) { // Try to delete the destination if (unlink(QFile::encodeName(dest).constData()) != 0) { - error(KIO::ERR_CANNOT_DELETE, dest); - return; + if (auto err = execWithElevatedPrivilege(DEL, dest)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE, dest); + } + return; + } } // Try again - this won't loop forever since unlink succeeded symlink(target, destUrl, flags); @@ -505,9 +613,13 @@ return; } } else { - // Some error occurred while we tried to symlink - error(KIO::ERR_CANNOT_SYMLINK, dest); - return; + if (auto err = execWithElevatedPrivilege(SYMLINK, dest, target)) { + if (!err.wasCanceled()) { + // Some error occurred while we tried to symlink + error(KIO::ERR_CANNOT_SYMLINK, dest); + } + return; + } } } finished(); @@ -525,14 +637,18 @@ // qDebug() << "Deleting file "<< url; if (unlink(_path.data()) == -1) { - if ((errno == EACCES) || (errno == EPERM)) { - error(KIO::ERR_ACCESS_DENIED, path); - } else if (errno == EISDIR) { - error(KIO::ERR_IS_DIRECTORY, path); - } else { - error(KIO::ERR_CANNOT_DELETE, path); + if (auto err = execWithElevatedPrivilege(DEL, _path)) { + if (!err.wasCanceled()) { + if ((errno == EACCES) || (errno == EPERM)) { + error(KIO::ERR_ACCESS_DENIED, path); + } else if (errno == EISDIR) { + error(KIO::ERR_IS_DIRECTORY, path); + } else { + error(KIO::ERR_CANNOT_DELETE, path); + } + } + return; } - return; } } else { @@ -547,11 +663,15 @@ } } if (QT_RMDIR(_path.data()) == -1) { - if ((errno == EACCES) || (errno == EPERM)) { - error(KIO::ERR_ACCESS_DENIED, path); - } else { - // qDebug() << "could not rmdir " << perror; - error(KIO::ERR_CANNOT_RMDIR, path); + if (auto err = execWithElevatedPrivilege(RMDIR, _path)) { + if (!err.wasCanceled()) { + if ((errno == EACCES) || (errno == EPERM)) { + error(KIO::ERR_ACCESS_DENIED, path); + } else { + // qDebug() << "could not rmdir " << perror; + error(KIO::ERR_CANNOT_RMDIR, path); + } + } return; } } @@ -594,16 +714,20 @@ } if (::chown(_path.constData(), uid, gid) == -1) { - switch (errno) { - case EPERM: - case EACCES: - error(KIO::ERR_ACCESS_DENIED, path); - break; - case ENOSPC: - error(KIO::ERR_DISK_FULL, path); - break; - default: - error(KIO::ERR_CANNOT_CHOWN, path); + if (auto err = execWithElevatedPrivilege(CHOWN, _path, uid, gid)) { + if (!err.wasCanceled()) { + switch (errno) { + case EPERM: + case EACCES: + error(KIO::ERR_ACCESS_DENIED, path); + break; + case ENOSPC: + error(KIO::ERR_DISK_FULL, path); + break; + default: + error(KIO::ERR_CANNOT_CHOWN, path); + } + } } } else { finished(); @@ -650,3 +774,54 @@ finished(); } + +PrivilegeOperationReturnValue FileProtocol::execWithElevatedPrivilege(ActionType action, const QVariant &arg1, + const QVariant &arg2, const QVariant &arg3) +{ + if (privilegeOperationUnitTestMode()) { + return PrivilegeOperationReturnValue::success(); + } + + if (!(errno == EACCES || errno == EPERM)) { + return PrivilegeOperationReturnValue::failure(); + } + + KIO::PrivilegeOperationStatus opStatus = requestPrivilegeOperation(); + if (opStatus != KIO::OperationAllowed) { + if (opStatus == KIO::OperationCanceled) { + error(KIO::ERR_USER_CANCELED, QString()); + return PrivilegeOperationReturnValue::canceled(); + } + return PrivilegeOperationReturnValue::failure(); + } + + if (action == CHMOD || action == CHOWN || action == UTIME) { + KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(arg1.toString()); + // Test for chmod and utime will return the same result as test for chown. + if (mp && !mp->testFileSystemFlag(KMountPoint::SupportsChown)) { + return PrivilegeOperationReturnValue::failure(); + } + } + + QByteArray helperArgs; + QDataStream out(&helperArgs, QIODevice::WriteOnly); + out << action << arg1 << arg2 << arg3; + + if (action == OPEN || action == OPENDIR) { + out << QVariant::fromValue(socketPath()); + } + + KAuth::Action execAction(QStringLiteral("org.kde.kio.file.exec")); + execAction.setHelperId(QStringLiteral("org.kde.kio.file")); + + QVariantMap argv; + argv.insert(QStringLiteral("arguments"), helperArgs); + execAction.setArguments(argv); + + auto reply = execAction.execute(); + if (reply->exec()) { + return PrivilegeOperationReturnValue::success(); + } + + return PrivilegeOperationReturnValue::failure(); +} diff --git a/src/ioslaves/file/file_win.cpp b/src/ioslaves/file/file_win.cpp --- a/src/ioslaves/file/file_win.cpp +++ b/src/ioslaves/file/file_win.cpp @@ -381,3 +381,23 @@ finished(); } + +bool FileProtocol::privilegeOperationUnitTestMode() +{ + return false; +} + +PrivilegeOperationReturnValue FileProtocol::execWithElevatedPrivilege(ActionType, const QVariant &, + const QVariant &, const QVariant &) +{ + return PrivilegeOperationReturnValue::failure(); +} +PrivilegeOperationReturnValue tryOpen(QFile &f, const QByteArray &, int , int) +{ + return PrivilegeOperationReturnValue::failure(); +} + +PrivilegeOperationReturnValue tryChangeFileAttr(ActionType , const QVariant &, const QVariant &, const QVariant &) +{ + return PrivilegeOperationReturnValue::failure(); +} diff --git a/src/ioslaves/file/kauth/CMakeLists.txt b/src/ioslaves/file/kauth/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/kauth/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(file_helper filehelper.cpp fdsender.cpp) +target_link_libraries(file_helper Qt5::Network KF5::Auth KF5::I18n KF5::KIOCore) + +install(TARGETS file_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) +kauth_install_helper_files(file_helper org.kde.kio.file root) +kauth_install_actions(org.kde.kio.file file.actions) diff --git a/src/ioslaves/file/kauth/fdsender.h b/src/ioslaves/file/kauth/fdsender.h new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/kauth/fdsender.h @@ -0,0 +1,37 @@ +/*** + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +***/ + +#ifndef FDSENDER_H +#define FDSENDER_H + +class FdSender +{ +public: + FdSender(const std::string &path); + ~FdSender(); + + bool sendFileDescriptor(int fd); + bool isConnected() const; + +private: + int m_socketDes; +}; + +#endif diff --git a/src/ioslaves/file/kauth/fdsender.cpp b/src/ioslaves/file/kauth/fdsender.cpp new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/kauth/fdsender.cpp @@ -0,0 +1,68 @@ +/*** + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +***/ + +#include +#include + +#include "../sharefd_p.h" +#include "fdsender.h" + +FdSender::FdSender(const std::string &path) + : m_socketDes(-1) +{ + m_socketDes = ::socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (m_socketDes == -1) { + std::cerr << "socket error:" << strerror(errno) << std::endl; + return; + } + + SocketAddress addr(path); + if (::connect(m_socketDes, addr.address(), addr.length()) != 0) { + std::cerr << "connection error:" << strerror(errno) << std::endl; + ::close(m_socketDes); + m_socketDes = -1; + return; + } +} + +FdSender::~FdSender() +{ + if (m_socketDes >= 0) { + ::close(m_socketDes); + } +} + +bool FdSender::sendFileDescriptor(int fd) +{ + FDMessageHeader msg; + cmsghdr *cmsg = msg.cmsgHeader(); + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_level = SOL_SOCKET; + memcpy(CMSG_DATA(cmsg), &fd, sizeof fd); + bool success = sendmsg(m_socketDes, msg.message(), 0) == 2; + ::close(m_socketDes); + return success; +} + +bool FdSender::isConnected() const +{ + return m_socketDes >= 0; +} diff --git a/src/ioslaves/file/kauth/file.actions b/src/ioslaves/file/kauth/file.actions new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/kauth/file.actions @@ -0,0 +1,5 @@ +[org.kde.kio.file.exec] +Name=Execute action as root. +Description=Root privileges are required to complete the action. +Policy=auth_admin +Persistence=session diff --git a/src/ioslaves/file/kauth/filehelper.h b/src/ioslaves/file/kauth/filehelper.h new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/kauth/filehelper.h @@ -0,0 +1,44 @@ +/*** + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +***/ + + +#ifndef FILEHELPER_H +#define FILEHELPER_H + +#include + +using namespace KAuth; + +/** + * This KAuth helper is responsible for performing file operations with + * root privileges. + */ +class FileHelper : public QObject +{ + Q_OBJECT + +public Q_SLOTS: + /** + * Execute action with root privileges. + **/ + ActionReply exec(const QVariantMap &args); +}; + +#endif diff --git a/src/ioslaves/file/kauth/filehelper.cpp b/src/ioslaves/file/kauth/filehelper.cpp new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/kauth/filehelper.cpp @@ -0,0 +1,139 @@ +/*** + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +***/ + +#include +#include +#include +#include +#include + +#include "filehelper.h" +#include "fdsender.h" +#include "../file_p.h" + +static bool sendFileDescriptor(int fd, const char *socketPath) +{ + FdSender fdSender(socketPath); + if (fdSender.isConnected() && fdSender.sendFileDescriptor(fd)) { + return true; + } + return false; +} + +ActionReply FileHelper::exec(const QVariantMap &args) +{ + ActionReply reply; + QByteArray data = args["arguments"].toByteArray(); + QDataStream in(data); + int action; + QVariant arg1, arg2, arg3, arg4; + in >> action >> arg1 >> arg2 >> arg3 >> arg4; + + // the path of an existing or a new file/dir upon which the method will operate + const QByteArray path = arg1.toByteArray(); + + switch(action) { + case CHMOD: { + int mode = arg2.toInt(); + if (chmod(path.data(), mode) == 0) { + return reply; + } + break; + } + case CHOWN: { + int uid = arg2.toInt(); + int gid = arg3.toInt(); + if (chown(path.data(), uid, gid) == 0) { + return reply; + } + break; + } + case DEL: { + if (unlink(path.data()) == 0) { + return reply; + } + break; + } + case MKDIR: { + if (mkdir(path.data(), 0777) == 0) { + return reply; + } + break; + } + case OPEN: { + int oflags = arg2.toInt(); + int mode = arg3.toInt(); + int fd = open(path.data(), oflags, mode); + bool success = (fd != -1) && sendFileDescriptor(fd, arg4.toByteArray().constData()); + close(fd); + if (success) { + return reply; + } + break; + } + case OPENDIR: { + DIR *dp = opendir(path.data()); + bool success = false; + if (dp) { + int fd = dirfd(dp); + success = (fd != -1) && sendFileDescriptor(fd, arg4.toByteArray().constData()); + closedir(dp); + if (success) { + return reply; + } + } + break; + } + case RENAME: { + const QByteArray newName = arg2.toByteArray(); + if (rename(path.data(), newName.data()) == 0) { + return reply; + } + break; + } + case RMDIR: { + if (rmdir(path.data()) == 0) { + return reply; + } + break; + } + case SYMLINK: { + const QByteArray target = arg2.toByteArray(); + if (symlink(target.data(), path.data()) == 0) { + return reply; + } + break; + } + case UTIME: { + utimbuf ut; + ut.actime = arg2.toULongLong(); + ut.modtime = arg3.toULongLong(); + if (utime(path.data(), &ut) == 0) { + return reply; + } + break; + } + }; + + reply.setError(errno ? errno : -1); + return reply; +} + +KAUTH_HELPER_MAIN("org.kde.kio.file", FileHelper) diff --git a/src/ioslaves/file/sharefd_p.h b/src/ioslaves/file/sharefd_p.h new file mode 100644 --- /dev/null +++ b/src/ioslaves/file/sharefd_p.h @@ -0,0 +1,83 @@ +/*** + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +***/ + +#include +#include + +class SocketAddress +{ + const sockaddr_un addr; + +public: + SocketAddress(const std::string &path) + : addr(make_address(path)) + { + } + + int length() const + { + return sizeof addr; + } + const sockaddr *address() const + { + return reinterpret_cast(&addr); + } + +private: + static sockaddr_un make_address(const std::string& path) + { + sockaddr_un a{ AF_UNIX, {0}}; + std::string finalPath = "/tmp/" + path; +#ifdef __linux__ + ::strcpy(&a.sun_path[1], finalPath.c_str()); +#else + ::strcpy(a.sun_path, finalPath.c_str()); + ::unlink(finalPath.c_str()); +#endif + return a; + } +}; + +class FDMessageHeader +{ + char io_buf[2]; + char cmsg_buf[CMSG_SPACE(sizeof(int))]; + iovec io; + msghdr msg; + +public: + FDMessageHeader() + : io_buf{0} + , cmsg_buf{0} + , io{io_buf, sizeof io_buf} + , msg{nullptr, 0, &io, 1, &cmsg_buf, sizeof cmsg_buf, 0} + { + } + + msghdr *message() + { + return &msg; + } + + cmsghdr *cmsgHeader() + { + return CMSG_FIRSTHDR(&msg); + } +};