File indexing completed on 2024-05-05 17:33:43

0001 /* vi: ts=8 sts=4 sw=4
0002  *
0003  * This file is part of the KDE project, module kdesu.
0004  * SPDX-FileCopyrightText: 1998 Pietro Iglio <iglio@fub.it>
0005  * SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org>
0006  * SPDX-License-Identifier: Artistic-2.0
0007  */
0008 
0009 #include <config-kde-cli-tools.h>
0010 
0011 #include <errno.h>
0012 #include <pwd.h>
0013 #include <stdlib.h>
0014 #include <string.h>
0015 #include <unistd.h>
0016 
0017 #include <sys/resource.h>
0018 #include <sys/time.h>
0019 #if HAVE_SYS_WAIT_H
0020 #include <sys/wait.h>
0021 #endif
0022 #if HAVE_SYS_PRCTL_H
0023 #include <sys/prctl.h>
0024 #endif
0025 #if HAVE_SYS_PROCCTL_H
0026 #include <sys/procctl.h>
0027 #include <unistd.h>
0028 #endif
0029 
0030 #include <QApplication>
0031 #include <QCommandLineParser>
0032 #include <QFile>
0033 #include <QFileInfo>
0034 #include <QLoggingCategory>
0035 
0036 #include <KSharedConfig>
0037 #include <kaboutdata.h>
0038 #include <kconfiggroup.h>
0039 #include <klocalizedstring.h>
0040 #include <kmessagebox.h>
0041 #include <kshell.h>
0042 #include <kstartupinfo.h>
0043 #include <kuser.h>
0044 #include <kwindowsystem.h>
0045 
0046 #include <kdesu/client.h>
0047 #include <kdesu/defaults.h>
0048 #include <kdesu/su.h>
0049 
0050 #include "sudlg.h"
0051 
0052 #define ERR strerror(errno)
0053 
0054 static QLoggingCategory category("org.kde.kdesu");
0055 
0056 QByteArray command;
0057 const char *Version = PROJECT_VERSION;
0058 
0059 // NOTE: if you change the position of the -u switch, be sure to adjust it
0060 // at the beginning of main()
0061 
0062 static int startApp(QCommandLineParser &p);
0063 
0064 int main(int argc, char *argv[])
0065 {
0066     // disable ptrace
0067 #if HAVE_PR_SET_DUMPABLE
0068     prctl(PR_SET_DUMPABLE, 0);
0069 #endif
0070 #if HAVE_PROC_TRACE_CTL
0071     int mode = PROC_TRACE_CTL_DISABLE;
0072     procctl(P_PID, getpid(), PROC_TRACE_CTL, &mode);
0073 #endif
0074 
0075     QApplication app(argc, argv);
0076 
0077     // FIXME: this can be considered a poor man's solution, as it's not
0078     // directly obvious to a gui user. :)
0079     // anyway, i vote against removing it even when we have a proper gui
0080     // implementation.  -- ossi
0081     QByteArray duser = qgetenv("ADMIN_ACCOUNT");
0082     if (duser.isEmpty()) {
0083         duser = "root";
0084     }
0085 
0086     KLocalizedString::setApplicationDomain("kdesu5");
0087 
0088     KAboutData aboutData(QStringLiteral("kdesu"),
0089                          i18n("KDE su"),
0090                          QLatin1String(Version),
0091                          i18n("Runs a program with elevated privileges."),
0092                          KAboutLicense::Artistic,
0093                          i18n("Copyright (c) 1998-2000 Geert Jansen, Pietro Iglio"));
0094     aboutData.addAuthor(i18n("Geert Jansen"), i18n("Maintainer"), QStringLiteral("jansen@kde.org"), QStringLiteral("http://www.stack.nl/~geertj/"));
0095     aboutData.addAuthor(i18n("Pietro Iglio"), i18n("Original author"), QStringLiteral("iglio@fub.it"));
0096     app.setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-password")));
0097 
0098     KAboutData::setApplicationData(aboutData);
0099 
0100     // NOTE: if you change the position of the -u switch, be sure to adjust it
0101     // at the beginning of main()
0102     QCommandLineParser parser;
0103     aboutData.setupCommandLine(&parser);
0104     parser.addPositionalArgument(QStringLiteral("command"), i18n("Specifies the command to run as separate arguments"));
0105     parser.addOption(QCommandLineOption(QStringLiteral("c"), i18n("Specifies the command to run as one string"), QStringLiteral("command")));
0106     parser.addOption(QCommandLineOption(QStringLiteral("f"), i18n("Run command under target uid if <file> is not writable"), QStringLiteral("file")));
0107     parser.addOption(QCommandLineOption(QStringLiteral("u"), i18n("Specifies the target uid"), QStringLiteral("user"), QString::fromLatin1(duser)));
0108     parser.addOption(QCommandLineOption(QStringLiteral("n"), i18n("Do not keep password")));
0109     parser.addOption(QCommandLineOption(QStringLiteral("s"), i18n("Stop the daemon (forgets all passwords)")));
0110     parser.addOption(QCommandLineOption(QStringLiteral("t"), i18n("Enable terminal output (no password keeping)")));
0111     parser.addOption(
0112         QCommandLineOption(QStringLiteral("p"), i18n("Set priority value: 0 <= prio <= 100, 0 is lowest"), QStringLiteral("prio"), QStringLiteral("50")));
0113     parser.addOption(QCommandLineOption(QStringLiteral("r"), i18n("Use realtime scheduling")));
0114     parser.addOption(QCommandLineOption(QStringLiteral("noignorebutton"), i18n("Do not display ignore button")));
0115     parser.addOption(QCommandLineOption(QStringLiteral("i"), i18n("Specify icon to use in the password dialog"), QStringLiteral("icon name")));
0116     parser.addOption(QCommandLineOption(QStringLiteral("d"), i18n("Do not show the command to be run in the dialog")));
0117 #ifdef HAVE_X11
0118     /* KDialog originally used --embed for attaching the dialog box.  However this is misleading and so we changed to --attach.
0119      * For consistancy, we silently map --embed to --attach */
0120     parser.addOption(QCommandLineOption(QStringLiteral("attach"),
0121                                         i18nc("Transient means that the kdesu app will be attached to the app specified by the winid so that it is like a "
0122                                               "dialog box rather than some separate program",
0123                                               "Makes the dialog transient for an X app specified by winid"),
0124                                         QStringLiteral("winid")));
0125     parser.addOption(QCommandLineOption(QStringLiteral("embed"), i18n("Embed into a window"), QStringLiteral("winid")));
0126 #endif
0127 
0128     // KApplication::disableAutoDcopRegistration();
0129     // kdesu doesn't process SM events, so don't even connect to ksmserver
0130     QByteArray session_manager = qgetenv("SESSION_MANAGER");
0131     if (!session_manager.isEmpty()) {
0132         unsetenv("SESSION_MANAGER");
0133     }
0134     // but propagate it to the started app
0135     if (!session_manager.isEmpty()) {
0136         setenv("SESSION_MANAGER", session_manager.data(), 1);
0137     }
0138 
0139     {
0140 #ifdef HAVE_X11
0141         KStartupInfoId id;
0142         id.initId();
0143         id.setupStartupEnv(); // make DESKTOP_STARTUP_ID env. var. available again
0144 #endif
0145     }
0146 
0147     parser.process(app);
0148     aboutData.processCommandLine(&parser);
0149     int result = startApp(parser);
0150 
0151     if (result == 127) {
0152         KMessageBox::error(nullptr, i18n("Cannot execute command '%1'.", QString::fromLocal8Bit(command)));
0153     }
0154     if (result == -2) {
0155         KMessageBox::error(nullptr, i18n("Cannot execute command '%1'. It contains invalid characters.", QString::fromLocal8Bit(command)));
0156     }
0157 
0158     return result;
0159 }
0160 
0161 static int startApp(QCommandLineParser &p)
0162 {
0163     // Stop daemon and exit?
0164     if (p.isSet(QStringLiteral("s"))) {
0165         KDEsuClient client;
0166         if (client.ping() == -1) {
0167             qCCritical(category) << "Daemon not running -- nothing to stop\n";
0168             p.showHelp(1);
0169         }
0170         if (client.stopServer() != -1) {
0171             qCDebug(category) << "Daemon stopped\n";
0172             exit(0);
0173         }
0174         qCCritical(category) << "Could not stop daemon\n";
0175         p.showHelp(1);
0176     }
0177 
0178     QString icon;
0179     if (p.isSet(QStringLiteral("i"))) {
0180         icon = p.value(QStringLiteral("i"));
0181     }
0182 
0183     bool prompt = true;
0184     if (p.isSet(QStringLiteral("d"))) {
0185         prompt = false;
0186     }
0187 
0188     // Get target uid
0189     const QByteArray user = p.value(QStringLiteral("u")).toLocal8Bit();
0190     QByteArray auth_user = user;
0191     struct passwd *pw = getpwnam(user.constData());
0192     if (pw == nullptr) {
0193         qCCritical(category) << "User " << user << " does not exist\n";
0194         p.showHelp(1);
0195     }
0196     bool other_uid = (getuid() != pw->pw_uid);
0197     bool change_uid = other_uid;
0198     if (!change_uid) {
0199         char *cur_user = getenv("USER");
0200         if (!cur_user) {
0201             cur_user = getenv("LOGNAME");
0202         }
0203         change_uid = (!cur_user || user != cur_user);
0204     }
0205 
0206     // If file is writeable, do not change uid
0207     QString file = p.value(QStringLiteral("f"));
0208     if (other_uid && !file.isEmpty()) {
0209         if (file.startsWith(QLatin1Char('/'))) {
0210             file = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, file);
0211             if (file.isEmpty()) {
0212                 qCCritical(category) << "Config file not found: " << file;
0213                 p.showHelp(1);
0214             }
0215         }
0216         QFileInfo fi(file);
0217         if (!fi.exists()) {
0218             qCCritical(category) << "File does not exist: " << file;
0219             p.showHelp(1);
0220         }
0221         change_uid = !fi.isWritable();
0222     }
0223 
0224     // Get priority/scheduler
0225     QString tmp = p.value(QStringLiteral("p"));
0226     bool ok;
0227     int priority = tmp.toInt(&ok);
0228     if (!ok || (priority < 0) || (priority > 100)) {
0229         qCCritical(category) << i18n("Illegal priority: %1", tmp);
0230         p.showHelp(1);
0231     }
0232     int scheduler = SuProcess::SchedNormal;
0233     if (p.isSet(QStringLiteral("r"))) {
0234         scheduler = SuProcess::SchedRealtime;
0235     }
0236     if ((priority > 50) || (scheduler != SuProcess::SchedNormal)) {
0237         change_uid = true;
0238         auth_user = "root";
0239     }
0240 
0241     // Get command
0242     if (p.isSet(QStringLiteral("c"))) {
0243         command = p.value(QStringLiteral("c")).toLocal8Bit();
0244         // Accepting additional arguments here is somewhat weird,
0245         // but one can conceive use cases: have a complex command with
0246         // redirections and additional file names which need to be quoted
0247         // safely.
0248     } else {
0249         if (p.positionalArguments().count() == 0) {
0250             qCCritical(category) << i18n("No command specified.");
0251             p.showHelp(1);
0252         }
0253     }
0254     const auto positionalArguments = p.positionalArguments();
0255     for (const QString &arg : positionalArguments) {
0256         command += ' ';
0257         command += QFile::encodeName(KShell::quoteArg(arg));
0258     }
0259 
0260     // Don't change uid if we're don't need to.
0261     if (!change_uid) {
0262         int result = system(command.constData());
0263         result = WEXITSTATUS(result);
0264         return result;
0265     }
0266 
0267     // Check for daemon and start if necessary
0268     bool just_started = false;
0269     bool have_daemon = true;
0270     KDEsuClient client;
0271     if (client.ping() == -1) {
0272         if (client.startServer() == -1) {
0273             qCWarning(category) << "Could not start daemon, reduced functionality.\n";
0274             have_daemon = false;
0275         }
0276         just_started = true;
0277     }
0278 
0279     // Try to exec the command with kdesud.
0280     bool keep = !p.isSet(QStringLiteral("n")) && have_daemon;
0281     bool terminal = p.isSet(QStringLiteral("t"));
0282     bool withIgnoreButton = !p.isSet(QStringLiteral("noignorebutton"));
0283     int winid = -1;
0284     bool attach = p.isSet(QStringLiteral("attach"));
0285     if (attach) {
0286         winid = p.value(QStringLiteral("attach")).toInt(&attach, 0); // C style parsing.  If the string begins with "0x", base 16 is used; if the string begins
0287                                                                      // with "0", base 8 is used; otherwise, base 10 is used.
0288         if (!attach) {
0289             qCWarning(category) << "Specified winid to attach to is not a valid number";
0290         }
0291     } else if (p.isSet(QStringLiteral("embed"))) {
0292         /* KDialog originally used --embed for attaching the dialog box.  However this is misleading and so we changed to --attach.
0293          * For consistancy, we silently map --embed to --attach */
0294         attach = true;
0295         winid = p.value(QStringLiteral("embed")).toInt(&attach, 0); // C style parsing.  If the string begins with "0x", base 16 is used; if the string begins
0296                                                                     // with "0", base 8 is used; otherwise, base 10 is used.
0297         if (!attach) {
0298             qCWarning(category) << "Specified winid to attach to is not a valid number";
0299         }
0300     }
0301 
0302     QList<QByteArray> env;
0303     QByteArray options;
0304     env << ("DESKTOP_STARTUP_ID=" + KStartupInfo::startupId());
0305 
0306     //     TODO: Maybe should port this to XDG_*, somehow?
0307     //     if (pw->pw_uid)
0308     //     {
0309     //        // Only propagate KDEHOME for non-root users,
0310     //        // root uses KDEROOTHOME
0311     //
0312     //        // Translate the KDEHOME of this user to the new user.
0313     //        QString kdeHome = KGlobal::dirs()->relativeLocation("home", KGlobal::dirs()->localkdedir());
0314     //        if (kdeHome[0] != '/')
0315     //           kdeHome.prepend("~/");
0316     //        else
0317     //           kdeHome.clear(); // Use default
0318     //
0319     //        env << ("KDEHOME="+ QFile::encodeName(kdeHome));
0320     //     }
0321 
0322     KUser u;
0323     env << (QByteArray)("KDESU_USER=" + u.loginName().toLocal8Bit());
0324 
0325     if (keep && !terminal && !just_started) {
0326         client.setPriority(priority);
0327         client.setScheduler(scheduler);
0328         int result = client.exec(command, user, options, env);
0329         if (result == 0) {
0330             result = client.exitCode();
0331             return result;
0332         }
0333     }
0334 
0335     // Set core dump size to 0 because we will have
0336     // root's password in memory.
0337     struct rlimit rlim;
0338     rlim.rlim_cur = rlim.rlim_max = 0;
0339     if (setrlimit(RLIMIT_CORE, &rlim)) {
0340         qCCritical(category) << "rlimit(): " << ERR;
0341         p.showHelp(1);
0342     }
0343 
0344     // Read configuration
0345     KConfigGroup config(KSharedConfig::openConfig(), "Passwords");
0346     int timeout = config.readEntry("Timeout", defTimeout);
0347 
0348     // Check if we need a password
0349     SuProcess proc;
0350     proc.setUser(auth_user);
0351     int needpw = proc.checkNeedPassword();
0352     if (needpw < 0) {
0353         QString err = i18n("Su returned with an error.\n");
0354         KMessageBox::error(nullptr, err);
0355         p.showHelp(1);
0356     }
0357     if (needpw == 0) {
0358         keep = 0;
0359         qDebug() << "Don't need password!!\n";
0360     }
0361 
0362     const QString cmd = QString::fromLocal8Bit(command);
0363     for (const QChar character : cmd) {
0364         if (!character.isPrint() && character.category() != QChar::Other_Surrogate) {
0365             return -2;
0366         }
0367     }
0368 
0369     // Start the dialog
0370     QString password;
0371     if (needpw) {
0372 #ifdef HAVE_X11
0373         KStartupInfoId id;
0374         id.initId();
0375         KStartupInfoData data;
0376         data.setSilent(KStartupInfoData::Yes);
0377         KStartupInfo::sendChange(id, data);
0378 #endif
0379         KDEsuDialog dlg(user, auth_user, keep && !terminal, icon, withIgnoreButton);
0380         if (prompt) {
0381             dlg.addCommentLine(i18n("Command:"), QFile::decodeName(command));
0382         }
0383         if (defKeep) {
0384             dlg.setKeepPassword(true);
0385         }
0386 
0387         if ((priority != 50) || (scheduler != SuProcess::SchedNormal)) {
0388             QString prio;
0389             if (scheduler == SuProcess::SchedRealtime) {
0390                 prio += i18n("realtime: ");
0391             }
0392             prio += QStringLiteral("%1/100").arg(priority);
0393             if (prompt) {
0394                 dlg.addCommentLine(i18n("Priority:"), prio);
0395             }
0396         }
0397 
0398         // Attach dialog
0399 #ifdef HAVE_X11
0400         if (attach) {
0401             dlg.setAttribute(Qt::WA_NativeWindow, true);
0402             KWindowSystem::setMainWindow(dlg.windowHandle(), WId(winid));
0403         }
0404 #endif
0405         int ret = dlg.exec();
0406         if (ret == KDEsuDialog::Rejected) {
0407 #ifdef HAVE_X11
0408             KStartupInfo::sendFinish(id);
0409 #endif
0410             p.showHelp(1);
0411         }
0412         if (ret == KDEsuDialog::AsUser) {
0413             change_uid = false;
0414         }
0415         password = dlg.password();
0416         keep = dlg.keepPassword();
0417 #ifdef HAVE_X11
0418         data.setSilent(KStartupInfoData::No);
0419         KStartupInfo::sendChange(id, data);
0420 #endif
0421     }
0422 
0423     // Some events may need to be handled (like a button animation)
0424     qApp->processEvents();
0425 
0426     // Run command
0427     if (!change_uid) {
0428         int result = system(command.constData());
0429         result = WEXITSTATUS(result);
0430         return result;
0431     } else if (keep && have_daemon) {
0432         client.setPass(password.toLocal8Bit().constData(), timeout);
0433         client.setPriority(priority);
0434         client.setScheduler(scheduler);
0435         int result = client.exec(command, user, options, env);
0436         if (result == 0) {
0437             result = client.exitCode();
0438             return result;
0439         }
0440     } else {
0441         SuProcess proc;
0442         proc.setTerminal(terminal);
0443         proc.setErase(true);
0444         proc.setUser(user);
0445         proc.setEnvironment(env);
0446         proc.setPriority(priority);
0447         proc.setScheduler(scheduler);
0448         proc.setCommand(command);
0449         int result = proc.exec(password.toLocal8Bit().constData());
0450         return result;
0451     }
0452     return -1;
0453 }