|
|
|
/* 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()
|
|
|
|
{
|
|
|
|
// 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("<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;
|
|
|
|
}
|
|
|
|
|