You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lxqt-admin-packaging/lxqt-admin-user/usermanager.cpp

437 lines
14 KiB

/* 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) <pcman.tw@gmail.com>
*
* 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 <QDebug>
#include <algorithm>
#include <QFileSystemWatcher>
#include <QTimer>
#include <QProcess>
#include <QFile>
#include <QMessageBox>
#include <unistd.h>
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()
{
// load groups
setgrent();
struct group * grp;
while((grp = getgrent())) {
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())) {
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("<strong>Action (%1) failed:</strong><br/><pre>%2</pre>").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;
}