diff --git a/autotests/src/katetextbuffertest.h b/autotests/src/katetextbuffertest.h --- a/autotests/src/katetextbuffertest.h +++ b/autotests/src/katetextbuffertest.h @@ -40,6 +40,7 @@ void foldingTest(); void nestedFoldingTest(); void saveFileInUnwritableFolder(); + void saveFileWithElevatedPrivileges(); }; #endif // KATEBUFFERTEST_H diff --git a/autotests/src/katetextbuffertest.cpp b/autotests/src/katetextbuffertest.cpp --- a/autotests/src/katetextbuffertest.cpp +++ b/autotests/src/katetextbuffertest.cpp @@ -466,3 +466,37 @@ QVERIFY(f.remove()); QVERIFY(dir.remove()); } + +void KateTextBufferTest::saveFileWithElevatedPrivileges() +{ + // create temp dir and get file name inside + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString file_path = dir.path() + QLatin1String("/foo"); + + QFile f(file_path); + QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Truncate)); + f.write("1234567890"); + QVERIFY(f.flush()); + f.close(); + + Kate::TextBuffer buffer(nullptr, 1, true); + buffer.setTextCodec(QTextCodec::codecForName("UTF-8")); + buffer.setFallbackTextCodec(QTextCodec::codecForName("UTF-8")); + bool a, b; + int c; + buffer.load(file_path, a, b, c, true); + buffer.clear(); + buffer.startEditing(); + buffer.insertText(KTextEditor::Cursor(0, 0), QLatin1String("ABC")); + buffer.finishEditing(); + qDebug() << buffer.text(); + buffer.save(file_path); + + f.open(QIODevice::ReadOnly); + QCOMPARE(f.readAll(), QByteArray("ABC")); + f.close(); + + QVERIFY(f.remove()); + QVERIFY(dir.remove()); +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -52,6 +52,7 @@ # KTextEditor interface sources set(ktexteditor_LIB_SRCS # text buffer & buffer helpers +buffer/katesecuretextbuffer.cpp buffer/katetextbuffer.cpp buffer/katetextblock.cpp buffer/katetextline.cpp @@ -334,5 +335,14 @@ ecm_generate_pri_file(BASE_NAME KTextEditor LIB_NAME KF5TextEditor DEPS "KParts" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KTextEditor) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) + +add_executable(kauth_ktexteditor_helper buffer/katesecuretextbuffer.cpp) +target_link_libraries(kauth_ktexteditor_helper + KF5::Auth +) +install(TARGETS kauth_ktexteditor_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR} ) +kauth_install_helper_files(kauth_ktexteditor_helper org.kde.ktexteditor.katetextbuffer root) +kauth_install_actions(org.kde.ktexteditor.katetextbuffer buffer/org.kde.ktexteditor.katetextbuffer.actions) + # add part add_subdirectory(part) diff --git a/src/buffer/katesecuretextbuffer.h b/src/buffer/katesecuretextbuffer.h new file mode 100644 --- /dev/null +++ b/src/buffer/katesecuretextbuffer.h @@ -0,0 +1,36 @@ +#ifndef KATE_SECURE_TEXTBUFFER_H +#define KATE_SECURE_TEXTBUFFER_H + +#include +#include + +#include + +using namespace KAuth; + +class SecureTextBuffer : public QObject +{ + Q_OBJECT + +public: + + SecureTextBuffer() {} + + ~SecureTextBuffer() {} + + static void setUnixPermissionsAndOwner(const QString &filename, const bool newFile, const uint ownerId, const uint groupId); + + /* + * Created for unit testing. + */ + static ActionReply movefileAction(const QVariantMap& args); + +private: + static bool moveFileInternal(const QString &sourceFile, const QString &targetFile, const uint ownerId, const uint groupId); + +public Q_SLOTS: + ActionReply movefile(const QVariantMap &args); + +}; + +#endif diff --git a/src/buffer/katesecuretextbuffer.cpp b/src/buffer/katesecuretextbuffer.cpp new file mode 100644 --- /dev/null +++ b/src/buffer/katesecuretextbuffer.cpp @@ -0,0 +1,78 @@ +#include "katesecuretextbuffer.h" + +#ifndef Q_OS_WIN +#include +#include + +// needed for umask application +#include +#include +#endif + +#include +#include +#include +#include +#include + +KAUTH_HELPER_MAIN("org.kde.ktexteditor.katetextbuffer", SecureTextBuffer) + +ActionReply SecureTextBuffer::movefile(const QVariantMap& args) +{ + return movefileAction(args); +} + +ActionReply SecureTextBuffer::movefileAction(const QVariantMap& args) +{ + const QString sourceFile = args[QLatin1String("sourceFile")].toString(); + const QString targetFile = args[QLatin1String("targetFile")].toString(); + const uint ownerId = (uint) args[QLatin1String("ownerId")].toInt(); + const uint groupId = (uint) args[QLatin1String("groupId")].toInt(); + + const bool ok = moveFileInternal(sourceFile, targetFile, ownerId, groupId); + + return ok ? ActionReply::SuccessReply() : ActionReply::HelperErrorReply(); +} + +void SecureTextBuffer::setUnixPermissionsAndOwner(const QString &filename, const bool newFile, const uint ownerId, const uint groupId) +{ + // QTemporaryFile sets permissions to 0600, so fixing this + if (newFile) { + const mode_t mask = umask(0); + umask(mask); + + const mode_t fileMode = 0666 & ~mask; + chmod(QFile::encodeName(filename).constData(), fileMode); + } + // ensure file has the same owner and group as before + if (!newFile && ownerId != (uint)-2 && groupId != (uint)-2) { + const int result = chown(QFile::encodeName(filename).constData(), ownerId, groupId); + // set at least correct group if owner cannot be changed + if (result != 0 && errno == EPERM) { + chown(QFile::encodeName(filename).constData(), getuid(), groupId); + } + } +} + +bool SecureTextBuffer::moveFileInternal(const QString &sourceFile, const QString &targetFile, const uint ownerId, const uint groupId) +{ + const bool newFile = !QFile::exists(targetFile); + + // remove target file if there is any + if (!newFile) { + if (!QFile::remove(targetFile)) { + return false; + } + } + + // move file + if (!QFile::rename(sourceFile, targetFile)) { + return false; + } + +#ifndef Q_OS_WIN + setUnixPermissionsAndOwner(targetFile, newFile, ownerId, groupId); +#endif + + return true; +} diff --git a/src/buffer/katetextbuffer.h b/src/buffer/katetextbuffer.h --- a/src/buffer/katetextbuffer.h +++ b/src/buffer/katetextbuffer.h @@ -71,7 +71,7 @@ * @param parent parent qobject * @param blockSize block size in lines the buffer should try to hold, default 64 lines */ - TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize = 64); + TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize = 64, bool alwaysUseKAuth = false); /** * Destruct the text buffer @@ -644,6 +644,11 @@ * Limit for line length, longer lines will be wrapped on load */ int m_lineLengthLimit; + + /** + * For unit-testing purposes only. + */ + bool m_alwaysUseKAuthForSave; }; } diff --git a/src/buffer/katetextbuffer.cpp b/src/buffer/katetextbuffer.cpp --- a/src/buffer/katetextbuffer.cpp +++ b/src/buffer/katetextbuffer.cpp @@ -19,8 +19,10 @@ */ #include "config.h" +#include "kateglobal.h" #include "katetextbuffer.h" +#include "katesecuretextbuffer.h" #include "katetextloader.h" // this is unfortunate, but needed for performance @@ -37,6 +39,9 @@ #endif #include +#include +#include +#include #if 0 #define BUFFER_DEBUG qCDebug(LOG_KTE) @@ -47,7 +52,7 @@ namespace Kate { -TextBuffer::TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize) +TextBuffer::TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize, bool alwaysUseKAuth) : QObject(parent) , m_document(parent) , m_history(*this) @@ -67,6 +72,7 @@ , m_endOfLineMode(eolUnix) , m_newLineAtEof(false) , m_lineLengthLimit(4096) + , m_alwaysUseKAuthForSave(KTextEditor::EditorPrivate::unitTestMode() ? alwaysUseKAuth : false) { // minimal block size must be > 0 Q_ASSERT(m_blockSize > 0); @@ -763,25 +769,53 @@ // codec must be set! Q_ASSERT(m_textCodec); + uint ownerId = -2; + uint groupId = -2; + #ifndef Q_OS_WIN const bool newFile = !QFile::exists(filename); + + /** + * Memorize owner and group. Due to design of QSaveFile we will have to re-set them after save is complete. + */ + if (!newFile) { + QFileInfo fileInfo(filename); + ownerId = fileInfo.ownerId(); + groupId = fileInfo.groupId(); + } #endif /** * use QSaveFile for save write + rename */ - QSaveFile saveFile(filename); - saveFile.setDirectWriteFallback(true); + QScopedPointer saveFile(new QSaveFile(filename)); + static_cast(saveFile.data())->setDirectWriteFallback(true); - if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - return false; + bool usingTemporaryFile = false; + + // open QSaveFile for write + if (m_alwaysUseKAuthForSave || !saveFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { + saveFile.reset(new QTemporaryFile()); + usingTemporaryFile = true; + + // open QTemporaryFile for write + if (!saveFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { + return false; + } + +#ifndef Q_OS_WIN + // set original file's permissions + if (!newFile) { + saveFile->setPermissions(QFile(filename).permissions()); + } +#endif } /** * construct correct filter device and try to open */ KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(m_mimeTypeForFilterDev); - KCompressionDevice file(&saveFile, false, type); + KCompressionDevice file(saveFile.data(), false, type); if (!file.open(QIODevice::WriteOnly)) { return false; @@ -836,37 +870,67 @@ file.close(); // flush file - if (!saveFile.flush()) { + if (!saveFile->flush()) { return false; } #ifndef Q_OS_WIN // ensure that the file is written to disk #ifdef HAVE_FDATASYNC - fdatasync(saveFile.handle()); + fdatasync(saveFile->handle()); #else - fsync(saveFile.handle()); + fsync(saveFile->handle()); #endif #endif // did save work? // only finalize if stream status == OK - bool ok = (stream.status() == QTextStream::Ok) && saveFile.commit(); + bool ok = (stream.status() == QTextStream::Ok); - // remember this revision as last saved if we had success! + // commit changes if (ok) { - m_history.setLastSavedRevision(); - } + + if (usingTemporaryFile) { + + // temporary file was used to save the file + // -> moving this file to original location with KAuth action + + QVariantMap args; + args[QLatin1String("sourceFile")] = saveFile->fileName(); + args[QLatin1String("targetFile")] = filename; + args[QLatin1String("ownerId")] = ownerId; + args[QLatin1String("groupId")] = groupId; + + // call save with elevated privileges + if (KTextEditor::EditorPrivate::unitTestMode()) { + ok = SecureTextBuffer::movefileAction(args).succeeded(); + } else { + KAuth::Action saveAction(QLatin1String("org.kde.ktexteditor.katetextbuffer.movefile")); + saveAction.setHelperId(QLatin1String("org.kde.ktexteditor.katetextbuffer")); + saveAction.setArguments(args); + KAuth::ExecuteJob *job = saveAction.execute(); + ok = job->exec(); + } + + } else { + + // standard save without elevated privileges + + ok = static_cast(saveFile.data())->commit(); #ifndef Q_OS_WIN - if (ok && newFile) { // QTemporaryFile sets permissions to 0600, so fixing this - const mode_t mask = umask(0); - umask(mask); + if (ok) { + SecureTextBuffer::setUnixPermissionsAndOwner(filename, newFile, ownerId, groupId); + } +#endif - const mode_t fileMode = 0666 & ~mask; - chmod(QFile::encodeName(filename).constData(), fileMode); + } + } + + // remember this revision as last saved if we had success! + if (ok) { + m_history.setLastSavedRevision(); } -#endif // report CODEC + ERRORS BUFFER_DEBUG << "Saved file " << filename << "with codec" << m_textCodec->name() << (ok ? "without" : "with") << "errors"; diff --git a/src/buffer/org.kde.ktexteditor.katetextbuffer.actions b/src/buffer/org.kde.ktexteditor.katetextbuffer.actions new file mode 100644 --- /dev/null +++ b/src/buffer/org.kde.ktexteditor.katetextbuffer.actions @@ -0,0 +1,10 @@ +[Domain] +Name=Document Actions +Policy=auth_admin +Persistence=session + +[org.kde.ktexteditor.katetextbuffer.movefile] +Name=Save Document +Description=Root privileges are needed to save this document +Policy=auth_admin +Persistence=session