/* BEGIN_COMMON_COPYRIGHT_HEADER * (c)LGPL2+ * * LXQt - a lightweight, Qt based, desktop toolset * http://lxqt.org * * Copyright: 2016 LXQt team * Authors: * Hong Jen Yee (PCMan) * * This program or 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) any later version. * * 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, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA * * END_COMMON_COPYRIGHT_HEADER */ #include "usermanager.h" #include #include #include #include #include #include #include #include static const QString PASSWD_FILE = QStringLiteral("/etc/passwd"); static const QString GROUP_FILE = QStringLiteral("/etc/group"); static const QString LOGIN_DEFS_FILE = QStringLiteral("/etc/login.defs"); UserManager::UserManager(QObject *parent): QObject(parent), mWatcher(new QFileSystemWatcher(QStringList() << PASSWD_FILE << GROUP_FILE, this)) { loadUsersAndGroups(); connect(mWatcher, &QFileSystemWatcher::fileChanged, this, &UserManager::onFileChanged); } UserManager::~UserManager() { qDeleteAll(mUsers); qDeleteAll(mGroups); } void UserManager::loadUsersAndGroups() { // Note: getpwent(), getgrent() makes no attempt to suppress duplicate information // if multiple sources are specified in nsswitch.conf(5). // load groups setgrent(); struct group * grp; while((grp = getgrent())) { if (mGroups.cend() != std::find_if(mGroups.cbegin(), mGroups.cend(), [grp] (const GroupInfo * g) -> bool { return g->gid() == grp->gr_gid; })) continue; GroupInfo* group = new GroupInfo(grp); mGroups.append(group); // add members of this group for(char** member_name = grp->gr_mem; *member_name; ++member_name) { group->addMember(QString::fromLatin1(*member_name)); } } endgrent(); std::sort(mGroups.begin(), mGroups.end(), [](GroupInfo* g1, GroupInfo* g2) { return g1->name() < g2->name(); }); // load users setpwent(); struct passwd * pw; while((pw = getpwent())) { if (mUsers.cend() != std::find_if(mUsers.cbegin(), mUsers.cend(), [pw] (const UserInfo * u) -> bool { return u->uid() == pw->pw_uid; })) continue; UserInfo* user = new UserInfo(pw); mUsers.append(user); // add groups to this user for(const GroupInfo* group: mGroups) { if(group->hasMember(user->name())) { user->addGroup(group->name()); } } } endpwent(); std::sort(mUsers.begin(), mUsers.end(), [](UserInfo*& u1, UserInfo*& u2) { return u1->name() < u2->name(); }); } // load settings from /etc/login.defs void UserManager::loadLoginDefs() { // FIXME: parse /etc/login.defs to get max UID, max system UID...etc. QFile file(LOGIN_DEFS_FILE); if(file.open(QIODevice::ReadOnly)) { while(!file.atEnd()) { QByteArray line = file.readLine().trimmed(); if(line.isEmpty() || line.startsWith('#')) continue; QStringList parts = QString::fromUtf8(line).split(QRegExp("\\s"), QString::SkipEmptyParts); if(parts.length() >= 2) { QString& key = parts[0]; QString& val = parts[1]; if(key == QLatin1Literal("SYS_UID_MIN")) { } else if(key == QLatin1Literal("SYS_UID_MAX")) { } else if(key == QLatin1Literal("UID_MIN")) { } else if(key == QLatin1Literal("UID_MAX")) { } else if(key == QLatin1Literal("SYS_GID_MIN")) { } else if(key == QLatin1Literal("SYS_GID_MAX")) { } else if(key == QLatin1Literal("GID_MIN")) { } else if(key == QLatin1Literal("GID_MAX")) { } } } file.close(); } } UserInfo* UserManager::findUserInfo(const char* name) { auto it = std::find_if(mUsers.begin(), mUsers.end(), [name](const UserInfo* user) { return user->name() == name; }); return it != mUsers.end() ? *it : nullptr; } UserInfo* UserManager::findUserInfo(QString name) { auto it = std::find_if(mUsers.begin(), mUsers.end(), [name](const UserInfo* user) { return user->name() == name; }); return it != mUsers.end() ? *it : nullptr; } UserInfo* UserManager::findUserInfo(uid_t uid) { auto it = std::find_if(mUsers.begin(), mUsers.end(), [uid](const UserInfo* user) { return user->uid() == uid; }); return it != mUsers.end() ? *it : nullptr; } GroupInfo* UserManager::findGroupInfo(const char* name) { auto it = std::find_if(mGroups.begin(), mGroups.end(), [name](const GroupInfo* group) { return group->name() == name; }); return it != mGroups.end() ? *it : nullptr; } GroupInfo* UserManager::findGroupInfo(QString name) { auto it = std::find_if(mGroups.begin(), mGroups.end(), [name](const GroupInfo* group) { return group->name() == name; }); return it != mGroups.end() ? *it : nullptr; } GroupInfo* UserManager::findGroupInfo(gid_t gid) { auto it = std::find_if(mGroups.begin(), mGroups.end(), [gid](const GroupInfo* group) { return group->gid() == gid; }); return it != mGroups.end() ? *it : nullptr; } void UserManager::reload() { mWatcher->addPath(PASSWD_FILE); mWatcher->addPath(GROUP_FILE); qDeleteAll(mUsers); // free the old UserInfo objects mUsers.clear(); qDeleteAll(mGroups); // free the old GroupInfo objects mGroups.clear(); loadUsersAndGroups(); Q_EMIT changed(); } void UserManager::onFileChanged(const QString &path) { // QFileSystemWatcher is very broken and has a ridiculous design. // we get "fileChanged()" when the file is deleted or modified, // but there is no way to distinguish them. If the file is deleted, // the QFileSystemWatcher stop working silently. Hence we workaround // this by remove the paths from the watcher and add them back again // to force the creation of new notifiers. mWatcher->removePath(PASSWD_FILE); mWatcher->removePath(GROUP_FILE); QTimer::singleShot(500, this, &UserManager::reload); } bool UserManager::pkexec(const QStringList& command, const QByteArray& stdinData) { Q_ASSERT(!command.isEmpty()); QProcess process; qDebug() << command; QStringList args; args << QStringLiteral("--disable-internal-agent") << QStringLiteral("lxqt-admin-user-helper") << command; process.start(QStringLiteral("pkexec"), args); if(!stdinData.isEmpty()) { process.waitForStarted(); process.write(stdinData); process.waitForBytesWritten(); process.closeWriteChannel(); } process.waitForFinished(-1); QByteArray pkexec_error = process.readAllStandardError(); qDebug() << pkexec_error; const bool succeeded = process.exitCode() == 0; if (!succeeded) { QMessageBox * msg = new QMessageBox{QMessageBox::Critical, tr("lxqt-admin-user") , tr("Action (%1) failed:
%2
").arg(command[0]).arg(pkexec_error.constData())}; msg->setAttribute(Qt::WA_DeleteOnClose, true); msg->show(); } return succeeded; } bool UserManager::addUser(UserInfo* user) { if(!user || user->name().isEmpty()) return false; QStringList command; command << QStringLiteral("useradd"); if(user->uid() != 0) { command << QStringLiteral("-u") << QString::number(user->uid()); } if(!user->homeDir().isEmpty()) { command << QStringLiteral("-d") << user->homeDir(); command << QStringLiteral("-m"); // create the user's home directory if it does not exist. } if(!user->shell().isEmpty()) { command << QStringLiteral("-s") << user->shell(); } if(!user->fullName().isEmpty()) { command << QStringLiteral("-c") << user->fullName(); } if(user->gid() != 0) { command << QStringLiteral("-g") << QString::number(user->gid()); } if(!user->groups().isEmpty()) { // set group membership command << QStringLiteral("-G") << user->groups().join(','); } #ifdef Q_OS_FREEBSD command << QStringLiteral("-n"); #endif command << user->name(); return pkexec(command); } bool UserManager::modifyUser(UserInfo* user, UserInfo* newSettings) { if(!user || user->name().isEmpty() || !newSettings) return false; bool isDirty = false; QStringList command; command << QStringLiteral("usermod"); if(newSettings->uid() != user->uid()) { command << QStringLiteral("-u") << QString::number(newSettings->uid()); isDirty=true; } if(newSettings->homeDir() != user->homeDir()) { command << QStringLiteral("-d") << newSettings->homeDir(); isDirty=true; } if(newSettings->shell() != user->shell()) { command << QStringLiteral("-s") << newSettings->shell(); isDirty=true; } if(newSettings->fullName() != user->fullName()) { command << QStringLiteral("-c") << newSettings->fullName(); isDirty=true; } if(newSettings->gid() != user->gid()) { command << QStringLiteral("-g") << QString::number(newSettings->gid()); isDirty=true; } if(newSettings->name() != user->name()) { // change login name command << QStringLiteral("-l") << newSettings->name(); isDirty=true; } if(newSettings->groups() != user->groups()) { // change group membership command << QStringLiteral("-G") << newSettings->groups().join(','); isDirty=true; } #ifdef Q_OS_FREEBSD command << QStringLiteral("-n"); #endif command << user->name(); if(isDirty) { return pkexec(command); } return true; //No changes } bool UserManager::deleteUser(UserInfo* user) { if(!user || user->name().isEmpty()) return false; QStringList command; command << QStringLiteral("userdel"); command << user->name(); return pkexec(command); } bool UserManager::changePassword(UserInfo* user, QByteArray newPasswd) { // In theory, the current user should be able to use "passwd" to // reset his/her own password without root permission, but... // /usr/bin/passwd is a setuid program running as root and QProcess // does not seem to capture its stdout... So... requires root for now. if(geteuid() == user->uid()) { // FIXME: there needs to be a way to let a user change his/her own password. // Maybe we can use our pkexec helper script to achieve this. } QStringList command; command << QStringLiteral("passwd"); command << user->name(); // we need to type the new password for two times. QByteArray stdinData; stdinData += newPasswd; stdinData += "\n"; stdinData += newPasswd; stdinData += "\n"; return pkexec(command, stdinData); } bool UserManager::addGroup(GroupInfo* group) { if(!group || group->name().isEmpty()) return false; QStringList command; command << QStringLiteral("groupadd"); if(group->gid() != 0) { command << QStringLiteral("-g") << QString::number(group->gid()); } command << group->name(); return pkexec(command); } bool UserManager::modifyGroup(GroupInfo* group, GroupInfo* newSettings) { if(!group || group->name().isEmpty() || !newSettings) return false; QStringList command; bool isDirty = false; command << QStringLiteral("groupmod"); if(newSettings->gid() != group->gid()) { command << QStringLiteral("-g") << QString::number(newSettings->gid()); isDirty = true; } if(newSettings->name() != group->name()) { isDirty = true; #ifdef Q_OS_FREEBSD command << QStringLiteral("-l"); #else command << QStringLiteral("-n"); #endif command << newSettings->name(); } #ifdef Q_OS_FREEBSD if(newSettings->members() != group->members()) { isDirty = true; command << QStringLiteral("-M"); // Set the list of group members. command << newSettings->members().join(','); } command << QStringLiteral("-n"); #endif command << group->name(); if(isDirty && !pkexec(command)) return false; // if group members are changed, use gpasswd to reset members on linux #ifndef Q_OS_FREEBSD //This is already done with pw groupmod -M earlier. if(newSettings->members() != group->members()) { command.clear(); command << QStringLiteral("gpasswd"); command << QStringLiteral("-M"); // Set the list of group members. command << newSettings->members().join(','); //if the group name changed the group->name() is still the old setting. if(newSettings->name() != group->name()) { command << newSettings->name(); } else { command << group->name(); } return pkexec(command); } #endif return true; } bool UserManager::deleteGroup(GroupInfo* group) { if(!group || group->name().isEmpty()) return false; QStringList command; command << QStringLiteral("groupdel"); command << group->name(); return pkexec(command); } bool UserManager::changePassword(GroupInfo* group, QByteArray newPasswd) { QStringList command; command << QStringLiteral("gpasswd"); command << group->name(); // we need to type the new password for two times. QByteArray stdinData = newPasswd; stdinData += "\n"; stdinData += newPasswd; stdinData += "\n"; return pkexec(command, stdinData); } const QStringList& UserManager::availableShells() { if(mAvailableShells.isEmpty()) { QFile file("/etc/shells"); if(file.open(QIODevice::ReadOnly)) { while(!file.atEnd()) { QByteArray line = file.readLine().trimmed(); if(line.isEmpty() || line.startsWith('#')) continue; mAvailableShells.append(QString::fromLocal8Bit(line)); } file.close(); } } return mAvailableShells; }