1 /*
2     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
3     SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
4 */
5 
6 #include "usermanager.h"
7 
8 #include <KUser>
9 #include <QFile>
10 #include <QRegularExpression>
11 #include <QProcess>
12 
13 #include <KAuthAction>
14 #include <KAuthExecuteJob>
15 #include <KLocalizedString>
16 
17 const auto everyoneUserName = QStringLiteral("Everyone");
18 
getUsersList()19 static QStringList getUsersList()
20 {
21     uid_t defminuid = 1000;
22     uid_t defmaxuid = 65000;
23 
24     QFile loginDefs(QStringLiteral("/etc/login.defs"));
25     if (loginDefs.open(QIODevice::ReadOnly | QIODevice::Text)) {
26         while (!loginDefs.atEnd()) {
27             const QString line = QString::fromLatin1(loginDefs.readLine());
28             {
29                 const QRegularExpression expression(QStringLiteral("^\\s*UID_MIN\\s+(?<UID_MIN>\\d+)"));
30                 const auto match = expression.match(line);
31                 if (match.hasMatch()) {
32                     defminuid = match.captured(u"UID_MIN").toUInt();
33                 }
34             }
35             {
36                 const QRegularExpression expression(QStringLiteral("^\\s*UID_MAX\\s+(?<UID_MAX>\\d+)"));
37                 const auto match = expression.match(line);
38                 if (match.hasMatch()) {
39                     defmaxuid = match.captured(u"UID_MAX").toUInt();
40                 }
41             }
42         }
43     }
44 
45     QStringList userList;
46     userList.append(everyoneUserName);
47     const QStringList userNames = KUser::allUserNames();
48     for (const QString &username : userNames) {
49         if (username == QLatin1String("nobody")) {
50             continue;
51         }
52         KUser user(username);
53         const uid_t nativeId = user.userId().nativeId();
54         if (nativeId >= defminuid && nativeId <= defmaxuid) {
55             userList << username;
56         }
57     }
58 
59     return userList;
60 }
61 
User(const QString & name,UserManager * parent)62 User::User(const QString &name, UserManager *parent)
63     : QObject(parent)
64     , m_name(name)
65 {
66 }
67 
name() const68 QString User::name() const
69 {
70     return m_name;
71 }
72 
inSamba() const73 bool User::inSamba() const
74 {
75     return m_inSamba;
76 }
77 
resolve()78 void User::resolve()
79 {
80     if (!qobject_cast<UserManager *>(parent())->canManageSamba() || m_name == everyoneUserName) {
81         // Assume the user is cool. If the auth backend isn't tbdsam it's likely delegated to a domain controller
82         // which in turn suggests that the local system is delegating all users to the controller. If not we
83         // can't do anything about it anyway the options are disable everything or pretend it'll work.
84         m_inSamba = true;
85         Q_EMIT resolved();
86         return;
87     }
88 
89     auto action = KAuth::Action(QStringLiteral("org.kde.filesharing.samba.isuserknown"));
90     action.setHelperId(QStringLiteral("org.kde.filesharing.samba"));
91     action.addArgument(QStringLiteral("username"), m_name);
92     // Detail message won't really show up unless the system admin forces authentication for this. Which would
93     // be very awkward
94     action.setDetailsV2({{
95         KAuth::Action::AuthDetail::DetailMessage,
96         i18nc("@label kauth action description %1 is a username", "Checking if Samba user '%1' exists", m_name) }
97     });
98     KAuth::ExecuteJob *job = action.execute();
99     connect(job, &KAuth::ExecuteJob::result, this, [this, job] {
100         job->deleteLater();
101         m_inSamba = job->data().value(QStringLiteral("exists"), false).toBool();
102         Q_EMIT inSambaChanged();
103         Q_EMIT resolved();
104     });
105     job->start();
106 }
107 
addToSamba(const QString & password)108 void User::addToSamba(const QString &password)
109 {
110     Q_ASSERT(qobject_cast<UserManager *>(parent())->canManageSamba());
111 
112     auto action = KAuth::Action(QStringLiteral("org.kde.filesharing.samba.createuser"));
113     action.setHelperId(QStringLiteral("org.kde.filesharing.samba"));
114     action.addArgument(QStringLiteral("username"), m_name);
115     action.addArgument(QStringLiteral("password"), password);
116     action.setDetailsV2({{
117         KAuth::Action::AuthDetail::DetailMessage,
118         i18nc("@label kauth action description %1 is a username", "Creating new Samba user '%1'", m_name) }
119     });
120     KAuth::ExecuteJob *job = action.execute();
121     connect(job, &KAuth::ExecuteJob::result, this, [this, job] {
122         job->deleteLater();
123         m_inSamba = job->data().value(QStringLiteral("created"), false).toBool();
124         if (!m_inSamba) {
125             Q_EMIT addToSambaError(job->data().value(QStringLiteral("stderr"), QString()).toString());
126         }
127         Q_EMIT inSambaChanged();
128     });
129     job->start();
130 }
131 
canManageSamba() const132 bool UserManager::canManageSamba() const
133 {
134     return m_canManageSamba;
135 }
136 
load()137 void UserManager::load()
138 {
139     auto proc = new QProcess(this);
140     proc->setProgram(QStringLiteral("testparm"));
141     proc->setArguments({
142         QStringLiteral("--debuglevel=0"),
143         QStringLiteral("--suppress-prompt"),
144         QStringLiteral("--verbose"),
145         QStringLiteral("--parameter-name"),
146         QStringLiteral("passdb backend")
147     });
148     connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this, proc](int exitCode) {
149         proc->deleteLater();
150         const QByteArray output = proc->readAllStandardOutput().simplified();
151 
152         if (exitCode == 0 && output == QByteArrayLiteral("tdbsam")) {
153             m_canManageSamba = true;
154         }
155 
156         const QString currentUserName = KUser().loginName();
157         const QStringList nameList = getUsersList();
158         for (const auto &name : nameList) {
159             ++m_waitingForResolution;
160             auto user = new User(name, this);
161             connect(user, &User::resolved, this, [this] {
162                 if (--m_waitingForResolution <= 0) {
163                     Q_EMIT loaded();
164                 }
165             }, Qt::QueuedConnection /* queue to ensure even shortcut resolution goes through the loop */);
166             m_users.append(user);
167             if (user->name() == currentUserName) {
168                 m_currentUser = user;
169             }
170             user->resolve();
171         }
172     });
173     proc->start();
174 }
175 
users() const176 QList<User *> UserManager::users() const
177 {
178     return m_users;
179 }
180 
currentUser() const181 Q_INVOKABLE User *UserManager::currentUser() const
182 {
183     return m_currentUser;
184 }
185