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

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003     SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
0004 */
0005 
0006 #include "authhelper.h"
0007 #include <KLocalizedString>
0008 #include <KUser>
0009 #include <QProcess>
0010 #include <QRegularExpression>
0011 #include <kauth/helpersupport.h>
0012 
0013 namespace
0014 {
0015 bool isValidUserName(const QString &name)
0016 {
0017     // https://systemd.io/USER_NAMES/
0018     static QRegularExpression expr(QStringLiteral("^[a-z_][a-z0-9_-]*$"));
0019     return expr.match(name).hasMatch();
0020 }
0021 
0022 std::optional<QString> callerUidToUserName()
0023 {
0024     const auto uid = KAuth::HelperSupport::callerUid();
0025     if (uid < 0) {
0026         return {};
0027     }
0028 
0029     const KUser user(static_cast<K_UID>(uid));
0030     if (!user.isValid()) {
0031         return {};
0032     }
0033 
0034     auto name = user.loginName();
0035     if (name.isEmpty()) {
0036         return {};
0037     }
0038     return name;
0039 }
0040 } // namespace
0041 
0042 ActionReply AuthHelper::isuserknown(const QVariantMap &args)
0043 {
0044     const auto username = args.value(QStringLiteral("username")).toString();
0045     if (!isValidUserName(username)) {
0046         auto reply = ActionReply::HelperErrorReply();
0047         reply.setErrorDescription(xi18nc("@info", "User name <resource>%1</resource> is not valid as the name of a Samba user; cannot check for its existence.", username));
0048         return reply;
0049     }
0050 
0051     QProcess p;
0052     const auto program = QStringLiteral("pdbedit");
0053     const auto arguments = QStringList({QStringLiteral("--debuglevel=0"), QStringLiteral("--user"), username });
0054     p.setProgram(program);
0055     p.setArguments(arguments);
0056     p.start();
0057     // Should be fairly quick: short timeout.
0058     const int pdbeditTimeout = 4000; // milliseconds
0059     p.waitForFinished(pdbeditTimeout);
0060 
0061     if (p.exitStatus() != QProcess::NormalExit) {
0062         // QByteArray can't do direct conversion to QString
0063         const QString errorText = QString::fromUtf8(p.readAllStandardOutput());
0064         auto reply = ActionReply::HelperErrorReply();
0065         reply.setErrorDescription(xi18nc("@info '%1 %2' together make up a terminal command; %3 is the command's output",
0066                                          "Command <command>%1 %2</command> failed:<nl/><nl/>%3", program, arguments.join(QLatin1Char(' ')), errorText));
0067         return reply;
0068     }
0069 
0070     ActionReply reply;
0071     reply.addData(QStringLiteral("exists"), p.exitCode() == 0);
0072     return reply;
0073 }
0074 
0075 ActionReply AuthHelper::createuser(const QVariantMap &args)
0076 {
0077     const auto username = callerUidToUserName();
0078     if (!username.has_value()) {
0079         auto reply = ActionReply::HelperErrorReply();
0080         reply.setErrorDescription(xi18nc("@info error while looking up uid from dbus", "Could not resolve calling user."));
0081         return reply;
0082     }
0083     const auto password = args.value(QStringLiteral("password")).toString();
0084     if (password.isEmpty()) {
0085         auto reply = ActionReply::HelperErrorReply();
0086         reply.setErrorDescription(i18nc("@info", "For security reasons, creating Samba users with empty passwords is not allowed."));
0087         return reply;
0088     }
0089 
0090     QProcess p;
0091     p.setProgram(QStringLiteral("smbpasswd"));
0092     p.setArguments({
0093         QStringLiteral("-L"), /* local mode */
0094         QStringLiteral("-s"), /* read from stdin */
0095         QStringLiteral("-D"), QStringLiteral("0"), /* force-disable debug */
0096         QStringLiteral("-a"), /* add user */
0097         username.value() });
0098     p.start();
0099     // despite being in silent mode we still need to write the password twice!
0100     p.write((password + QStringLiteral("\n")).toUtf8());
0101     p.write((password + QStringLiteral("\n")).toUtf8());
0102     p.waitForBytesWritten();
0103     p.closeWriteChannel();
0104     p.waitForFinished();
0105 
0106     if (p.exitStatus() != QProcess::NormalExit) {
0107         auto reply = ActionReply::HelperErrorReply();
0108         reply.setErrorDescription(QString::fromUtf8(p.readAllStandardError()));
0109         return reply;
0110     }
0111 
0112     ActionReply reply;
0113     reply.addData(QStringLiteral("created"), p.exitCode() == 0);
0114     // stderr will generally contain info on what went wrong so forward it
0115     // so the UI may display it
0116     reply.addData(QStringLiteral("stderr"), p.readAllStandardError());
0117     return reply;
0118 }
0119 
0120 ActionReply AuthHelper::addtogroup(const QVariantMap &args)
0121 {
0122     const auto group = args.value(QStringLiteral("group")).toString();
0123     const auto user = callerUidToUserName();
0124     if (!user.has_value()) {
0125         auto reply = ActionReply::HelperErrorReply();
0126         reply.setErrorDescription(xi18nc("@info error while looking up uid from dbus", "Could not resolve calling user."));
0127         return reply;
0128     }
0129     if (!isValidUserName(group)) {
0130         auto reply = ActionReply::HelperErrorReply();
0131         reply.setErrorDescription(xi18nc("@info", "<resource>%1</resource> is not a valid group name; cannot make user <resource>%2</resource> a member of it.", group, user.value()));
0132         return reply;
0133     }
0134     // Harden against some input abuse.
0135     // Keep this condition in sync with the one in groupmanager.cpp
0136     if (group.contains(QLatin1String("admin")) ||
0137         group.contains(QLatin1String("root"))) {
0138         auto reply = ActionReply::HelperErrorReply();
0139         reply.setErrorDescription(xi18nc("@info", "For security reasons, cannot make user <resource>%1</resource> a member of group <resource>%2</resource>. \
0140                                                    The group name is insecure; valid group names do not \
0141                                                    include the text <resource>admin</resource> or <resource>root</resource>."));
0142         return reply;
0143     }
0144 
0145     QProcess p;
0146 #if defined(Q_OS_FREEBSD)
0147     p.setProgram(QStringLiteral("pw"));
0148     p.setArguments({
0149         QStringLiteral("group"),
0150         QStringLiteral("mod"),
0151         QStringLiteral("{%1}").arg(group),
0152         QStringLiteral("-m"),
0153         QStringLiteral("{%1}").arg(user.value()) });
0154 #elif defined(Q_OS_LINUX) || defined(Q_OS_HURD)
0155     p.setProgram(QStringLiteral("/usr/sbin/usermod"));
0156     p.setArguments({
0157         QStringLiteral("--append"),
0158         QStringLiteral("--groups"),
0159         group,
0160         user.value() });
0161 #else
0162 #   error "Platform lacks group management support. Please add support."
0163 #endif
0164 
0165     p.start();
0166     p.waitForFinished(1000);
0167 
0168     if (p.exitCode() != 0 || p.exitStatus() != QProcess::NormalExit) {
0169         auto reply = ActionReply::HelperErrorReply();
0170         reply.setErrorDescription(QString::fromUtf8(p.readAll()));
0171         return reply;
0172     }
0173 
0174     return ActionReply::SuccessReply();
0175 }
0176 
0177 KAUTH_HELPER_MAIN("org.kde.filesharing.samba", AuthHelper)