diff --git a/CMakeLists.txt b/CMakeLists.txt index 42c80be..f723308 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,85 +1,89 @@ cmake_minimum_required(VERSION 3.5) set(KF5_VERSION "5.54.0") # handled by release scripts set(KF5_DEP_VERSION "5.54.0") # handled by release scripts project(KDESu VERSION ${KF5_VERSION}) include(FeatureSummary) find_package(ECM 5.54.0 NO_MODULE) set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) set(REQUIRED_QT_VERSION 5.10.0) find_package(Qt5Core ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) find_package(KF5CoreAddons ${KF5_DEP_VERSION} REQUIRED) find_package(KF5I18n ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Service ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Pty ${KF5_DEP_VERSION} REQUIRED) #optional features find_package(X11) set(HAVE_X11 ${X11_FOUND}) include(GenerateExportHeader) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMMarkNonGuiExecutable) include(ECMAddQch) option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") ecm_setup_version(PROJECT VARIABLE_PREFIX KDESU VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kdesu_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5SuConfigVersion.cmake" SOVERSION 5) if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/po") ki18n_install(po) endif() add_subdirectory(src) # create a Config.cmake and a ConfigVersion.cmake file and install them set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Su") if (BUILD_QCH) ecm_install_qch_export( TARGETS KF5Su_QCH FILE KF5SuQchTargets.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KF5SuQchTargets.cmake\")") endif() include(CMakePackageConfigHelpers) configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KF5SuConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5SuConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5SuConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KF5SuConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) install(EXPORT KF5SuTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KF5SuTargets.cmake NAMESPACE KF5:: ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kdesu_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel ) +if(BUILD_TESTING) + add_subdirectory(autotests) +endif() + feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 index 0000000..7f56f4b --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,4 @@ +include(ECMAddTests) +find_package(Qt5Test REQUIRED) +configure_file(config-kdesutest.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kdesutest.h) +ecm_add_test(kdesutest.cpp TEST_NAME kdesutest LINK_LIBRARIES Qt5::Test KF5::Su KF5::CoreAddons KF5::ConfigCore) diff --git a/autotests/config-kdesutest.h.cmake b/autotests/config-kdesutest.h.cmake new file mode 100644 index 0000000..5f320cc --- /dev/null +++ b/autotests/config-kdesutest.h.cmake @@ -0,0 +1,2 @@ +#define CMAKE_HOME_DIRECTORY "${CMAKE_HOME_DIRECTORY}" +#define CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" diff --git a/autotests/kdesutest.cpp b/autotests/kdesutest.cpp new file mode 100644 index 0000000..0ef40a4 --- /dev/null +++ b/autotests/kdesutest.cpp @@ -0,0 +1,98 @@ +/* + * Copyright 2019 Jonathan Riddell + * + * 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 . + */ + +#define MYPASSWORD "ilovekde" +#define ROOTPASSWORD "ilovekde" +#include "config-kdesutest.h" + +#include +#include +#include + +#include +#include +#include + +#include "suprocess.h" + +namespace KDESu +{ + +class KdeSuTest: public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() { + QStandardPaths::setTestModeEnabled(true); + } + + void editConfig(QString command, QString commandPath) { + KSharedConfig::Ptr config = KSharedConfig::openConfig(); + KConfigGroup group(config, "super-user-command"); + group.writeEntry("super-user-command", command); + QString kdesuStubPath = QString::fromLocal8Bit(CMAKE_RUNTIME_OUTPUT_DIRECTORY) + QString::fromLocal8Bit("/kdesu_stub"); + group.writeEntry("kdesu_stub_path", kdesuStubPath); + group.writeEntry("command", commandPath); + } + + void sudoGoodPassword() { + editConfig(QString::fromLocal8Bit("sudo"), QString::fromLocal8Bit(CMAKE_HOME_DIRECTORY) + QString::fromLocal8Bit("/autotests/sudo")); + + KDESu::SuProcess *suProcess = new KDESu::SuProcess("root", "ls"); + QString suapp = suProcess->superUserCommand(); + QVERIFY(suapp==QLatin1String("sudo")); + int result = suProcess->exec(MYPASSWORD, 0); + QVERIFY(result == 0); + } + + void sudoBadPassword() { + editConfig(QString::fromLocal8Bit("sudo"), QString::fromLocal8Bit(CMAKE_HOME_DIRECTORY) + QString::fromLocal8Bit("/autotests/sudo")); + + KDESu::SuProcess *suProcess = new KDESu::SuProcess("root", "ls"); + QString suapp = suProcess->superUserCommand(); + QVERIFY(suapp==QLatin1String("sudo")); + int result2 = suProcess->exec("broken", 0); + QVERIFY(result2 == KDESu::SuProcess::SuIncorrectPassword); + } + + void suGoodPassword() { + editConfig(QString::fromLocal8Bit("su"), QString::fromLocal8Bit(CMAKE_HOME_DIRECTORY) + QString::fromLocal8Bit("/autotests/su")); + + KDESu::SuProcess *suProcess = new KDESu::SuProcess("root", "ls"); + QString suapp = suProcess->superUserCommand(); + QVERIFY(suapp==QLatin1String("su")); + int result2 = suProcess->exec(ROOTPASSWORD, 0); + QVERIFY(result2 == 0); + } + + void suBadPassword() { + editConfig(QString::fromLocal8Bit("su"), QString::fromLocal8Bit(CMAKE_HOME_DIRECTORY) + QString::fromLocal8Bit("/autotests/su")); + + KDESu::SuProcess *suProcess = new KDESu::SuProcess("root", "ls"); + QString suapp = suProcess->superUserCommand(); + QVERIFY(suapp==QLatin1String("su")); + int result2 = suProcess->exec("broken", 0); + QVERIFY(result2 == KDESu::SuProcess::SuIncorrectPassword); + } +}; +} + +#include +QTEST_MAIN(KDESu::KdeSuTest) diff --git a/autotests/su b/autotests/su new file mode 100755 index 0000000..45cdc3c --- /dev/null +++ b/autotests/su @@ -0,0 +1,38 @@ +#!/usr/bin/python3 + +import sys +import getpass +from enum import Enum, unique +from subprocess import call + + +@unique +class State(Enum): + NEW = 1 + SECOND = 2 + THIRD = 3 + GOOD = 4 + FAIL = 5 + +class Su: + + def __init__(self): + self.state = State.NEW + self.read = None + self.password = 'ilovekde' + + def process(self): + if self.state == State.NEW: + self.read = getpass.getpass('Password: ') + if self.read == self.password: + self.state = State.GOOD + call([sys.argv[3]]) + exit(0) + else: + self.state = State.FAIL + print("su: Authentication failure") + exit(1) + +su = Su() +while True: + su.process() diff --git a/autotests/sudo b/autotests/sudo new file mode 100755 index 0000000..b6ca96f --- /dev/null +++ b/autotests/sudo @@ -0,0 +1,54 @@ +#!/usr/bin/python3 + +import sys +import getpass +from enum import Enum, unique +from subprocess import call + + +@unique +class State(Enum): + NEW = 1 + SECOND = 2 + THIRD = 3 + GOOD = 4 + FAIL = 5 + +class Sudo: + + def __init__(self): + self.state = State.NEW + self.read = None + self.password = 'ilovekde' + + def process(self): + if self.state == State.NEW: + self.read = getpass.getpass('[sudo] password for jr: ') + if self.read == self.password: + self.state = State.GOOD + call([sys.argv[3]]) + exit(0) + else: + self.state = State.SECOND + elif self.state == State.SECOND: + print('Sorry, try again.') + self.read = getpass.getpass('[sudo] password for jr: ') + if self.read == self.password: + self.state = State.GOOD + exit(0) + else: + self.state = State.THIRD + elif self.state == State.THIRD: + print('Sorry, try again.') + self.read = getpass.getpass('[sudo] password for jr: ') + if self.read == self.password: + self.state = State.GOOD + exit(0) + else: + print("sudo: 3 incorrect password attempts") + self.state = State.FAIL + exit(1) + +sudo = Sudo() +while True: + sudo.process() diff --git a/src/suprocess.cpp b/src/suprocess.cpp index f75a1f1..0e653f1 100644 --- a/src/suprocess.cpp +++ b/src/suprocess.cpp @@ -1,279 +1,286 @@ /* * This file is part of the KDE project, module kdesu. * Copyright (C) 1999,2000 Geert Jansen * * Sudo support added by Jonathan Riddell * Copyright (C) 2005 Canonical Ltd // krazy:exclude=copyright (no email) * * This is free software; you can use this library under the GNU Library * General Public License, version 2. See the file "COPYING.LIB" for the * exact licensing terms. * * su.cpp: Execute a program as another user with "class SuProcess". */ #include "suprocess.h" #include "kcookie_p.h" #include #include #include #include #include #include #include #include #ifdef KDESU_USE_SUDO_DEFAULT # define DEFAULT_SUPER_USER_COMMAND QStringLiteral("sudo") #else # define DEFAULT_SUPER_USER_COMMAND QStringLiteral("su") #endif namespace KDESu { using namespace KDESuPrivate; class Q_DECL_HIDDEN SuProcess::SuProcessPrivate { public: QString superUserCommand; }; SuProcess::SuProcess(const QByteArray &user, const QByteArray &command) : d(new SuProcessPrivate) { m_user = user; m_command = command; KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group(config, "super-user-command"); d->superUserCommand = group.readEntry("super-user-command", DEFAULT_SUPER_USER_COMMAND); if (d->superUserCommand != QLatin1String("sudo") && d->superUserCommand != QLatin1String("su")) { qWarning() << "unknown super user command."; d->superUserCommand = DEFAULT_SUPER_USER_COMMAND; } } SuProcess::~SuProcess() { delete d; } QString SuProcess::superUserCommand() { return d->superUserCommand; } bool SuProcess::useUsersOwnPassword() { if (superUserCommand() == QLatin1String("sudo") && m_user == "root") { return true; } KUser user; return user.loginName() == QString::fromUtf8(m_user); } int SuProcess::checkInstall(const char *password) { return exec(password, Install); } int SuProcess::checkNeedPassword() { return exec(nullptr, NeedPassword); } /* * Execute a command with su(1). */ int SuProcess::exec(const char *password, int check) { if (check) { setTerminal(true); } // since user may change after constructor (due to setUser()) // we need to override sudo with su for non-root here if (m_user != QByteArray("root")) { d->superUserCommand = QStringLiteral("su"); } QList args; if (d->superUserCommand == QLatin1String("sudo")) { args += "-u"; } if (m_scheduler != SchedNormal || m_priority > 50) { args += "root"; } else { args += m_user; } if (d->superUserCommand == QLatin1String("su")) { args += "-c"; } - args += QByteArray(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5) + "/kdesu_stub"; + // Get the kdesu_stub and su command from a config file if set, used in test + KSharedConfig::Ptr config = KSharedConfig::openConfig(); + KConfigGroup group(config, "super-user-command"); + const QString defaultPath = QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5) + QStringLiteral("/kdesu_stub"); + const QString kdesuStubPath = group.readEntry("kdesu_stub_path", defaultPath); + args += kdesuStubPath.toLocal8Bit(); args += "-"; // krazy:exclude=doublequote_chars (QList, not QString) - const QByteArray command = QFile::encodeName(QStandardPaths::findExecutable(d->superUserCommand)); + const QString commandString = group.readEntry("command", QStandardPaths::findExecutable(d->superUserCommand)); + const QByteArray command = commandString.toLocal8Bit(); if (command.isEmpty()) { return check ? SuNotFound : -1; } if (StubProcess::exec(command, args) < 0) { return check ? SuNotFound : -1; } SuErrors ret = (SuErrors)converseSU(password); if (ret == error) { if (!check) { qCritical() << "[" << __FILE__ << ":" << __LINE__ << "] " << "Conversation with su failed."; } return ret; } if (check == NeedPassword) { if (ret == killme) { if (d->superUserCommand == QLatin1String("sudo")) { // sudo can not be killed, just return return ret; } if (kill(m_pid, SIGKILL) < 0) { //FIXME SIGKILL doesn't work for sudo, //why is this different from su? //A: because sudo runs as root. Perhaps we could write a Ctrl+C to its stdin, instead? ret = error; } else { int iret = waitForChild(); if (iret < 0) { ret = error; } } } return ret; } if (m_erase && password) { memset(const_cast(password), 0, qstrlen(password)); } if (ret != ok) { kill(m_pid, SIGKILL); if (d->superUserCommand != QLatin1String("sudo")) { waitForChild(); } return SuIncorrectPassword; } int iret = converseStub(check); if (iret < 0) { if (!check) { qCritical() << "[" << __FILE__ << ":" << __LINE__ << "] " << "Conversation with kdesu_stub failed."; } return iret; } else if (iret == 1) { kill(m_pid, SIGKILL); waitForChild(); return SuIncorrectPassword; } if (check == Install) { waitForChild(); return 0; } iret = waitForChild(); return iret; } /* * Conversation with su: feed the password. * Return values: -1 = error, 0 = ok, 1 = kill me, 2 not authorized */ int SuProcess::converseSU(const char *password) { enum { WaitForPrompt, CheckStar, HandleStub } state = WaitForPrompt; int colon; unsigned i, j; QByteArray line; while (true) { line = readLine(); - if (line.isNull()) { + // return if problem. sudo checks for a second prompt || su gets a blank line + if ((line.contains(':') && state != WaitForPrompt) || line.isNull()) { return (state == HandleStub ? notauthorized : error); } if (line == "kdesu_stub") { unreadLine(line); return ok; } switch (state) { case WaitForPrompt: { if (waitMS(fd(), 100) > 0) { // There is more output available, so this line // couldn't have been a password prompt (the definition // of prompt being that there's a line of output followed // by a colon, and then the process waits). continue; } const uint len = line.length(); // Match "Password: " with the regex ^[^:]+:[\w]*$. for (i = 0, j = 0, colon = 0; i < len; ++i) { if (line[i] == ':') { j = i; colon++; continue; } if (!isspace(line[i])) { j++; } } if (colon == 1 && line[j] == ':') { if (password == nullptr) { return killme; } if (waitSlave()) { return error; } write(fd(), password, strlen(password)); write(fd(), "\n", 1); state = CheckStar; } break; } ////////////////////////////////////////////////////////////////////////// case CheckStar: { QByteArray s = line.trimmed(); if (s.isEmpty()) { state = HandleStub; break; } const uint len = line.length(); for (i = 0; i < len; ++i) { if (s[i] != '*') { return error; } } state = HandleStub; break; } ////////////////////////////////////////////////////////////////////////// case HandleStub: break; ////////////////////////////////////////////////////////////////////////// } // end switch } // end while (true) return ok; } void SuProcess::virtual_hook(int id, void *data) { StubProcess::virtual_hook(id, data); } } // namespace KDESu