/* SPDX-FileCopyrightText: 2019 Nicolas Fella SPDX-FileCopyrightText: 2020 Carson Black SPDX-FileCopyrightText: 2020 David Redondo SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ #include "user.h" #include "config-users.h" #include "kcmusers_debug.h" #include "user_interface.h" #include #include #include #include #include #include #include #include #if HAVE_CRYPT_GENSALT #include #endif using namespace Qt::StringLiterals; User::User(QObject *parent) : QObject(parent) { } qulonglong User::uid() const { return mUid; } void User::setUid(qulonglong value) { if (mUid == value) { return; } mUid = value; Q_EMIT uidChanged(); } QString User::name() const { return mName; } void User::setName(const QString &value) { if (mName == value) { return; } mName = value; Q_EMIT nameChanged(); Q_EMIT displayNamesChanged(); } QString User::realName() const { return mRealName; } void User::setRealName(const QString &value) { if (mRealName == value) { return; } mRealName = value; Q_EMIT realNameChanged(); Q_EMIT displayNamesChanged(); } QString User::displayPrimaryName() const { return !mRealName.isEmpty() ? mRealName : mName; } QString User::displaySecondaryName() const { return !mRealName.isEmpty() ? mName : QString(); } QString User::email() const { return mEmail; } void User::setEmail(const QString &value) { if (mEmail == value) { return; } mEmail = value; Q_EMIT emailChanged(); } QUrl User::face() const { return mFace; } bool User::faceValid() const { return mFaceValid; } void User::setFace(const QUrl &value) { if (value.toString().contains(PLASMA_AVATAR_PATH) || value.toString().contains(u"/var/lib/AccountsService/icons")) { mFace = value; } else { const QString croppedImagesCache = QString::fromUtf8(PLASMA_FACE_CACHE_PATH); const QString faceFile = croppedImagesCache + QString::number(QRandomGenerator::global()->bounded(8192)) + u".png"; if (!QDir(croppedImagesCache).exists()) { QDir().mkpath(croppedImagesCache); } QFile tempFile(faceFile); QImageWriter writer(faceFile); QImage image(value.toLocalFile()); image = image.copy(mFaceCrop); writer.write(image); mFace = QUrl(QString(u"file://" + faceFile)); } mFaceValid = QFile::exists(value.path()); Q_EMIT faceValidChanged(); Q_EMIT faceChanged(); } bool User::administrator() const { return mAdministrator; } void User::setAdministrator(bool value) { if (mAdministrator == value) { return; } mAdministrator = value; Q_EMIT administratorChanged(); } void User::setPath(const QDBusObjectPath &path) { if (!m_dbusIface.isNull()) { delete m_dbusIface; } m_dbusIface = new OrgFreedesktopAccountsUserInterface(QStringLiteral("org.freedesktop.Accounts"), path.path(), QDBusConnection::systemBus(), this); m_dbusIface->setInteractiveAuthorizationAllowed(true); if (m_dbusIface->systemAccount()) { return; } mPath = path; connect(m_dbusIface, &OrgFreedesktopAccountsUserInterface::Changed, [this]() { loadData(); }); loadData(); } void User::loadData() { bool userDataChanged = false; if (mUid != m_dbusIface->uid()) { mUid = m_dbusIface->uid(); mOriginalUid = mUid; userDataChanged = true; Q_EMIT uidChanged(); } if (mName != m_dbusIface->userName()) { mName = m_dbusIface->userName(); mOriginalName = mName; userDataChanged = true; Q_EMIT nameChanged(); } const auto localIconFile = QUrl::fromLocalFile(m_dbusIface->iconFile()); if (mFace != localIconFile) { mFace = localIconFile; mOriginalFace = mFace; mFaceValid = QFileInfo::exists(mFace.toString()); mOriginalFaceValid = mFaceValid; userDataChanged = true; Q_EMIT faceChanged(); Q_EMIT faceValidChanged(); } if (mRealName != m_dbusIface->realName()) { mRealName = m_dbusIface->realName(); mOriginalRealName = mRealName; userDataChanged = true; Q_EMIT realNameChanged(); } if (mEmail != m_dbusIface->email()) { mEmail = m_dbusIface->email(); mOriginalEmail = mEmail; userDataChanged = true; Q_EMIT emailChanged(); } const auto administrator = (m_dbusIface->accountType() == 1); if (mAdministrator != administrator) { mAdministrator = administrator; mOriginalAdministrator = mAdministrator; userDataChanged = true; Q_EMIT administratorChanged(); } const auto loggedIn = (mUid == getuid()); if (mLoggedIn != loggedIn) { mLoggedIn = loggedIn; mOriginalLoggedIn = mLoggedIn; userDataChanged = true; } if (userDataChanged) { Q_EMIT dataChanged(); } } #if !(HAVE_CRYPT_GENSALT) static QLatin1Char saltCharacter() { static constexpr quint32 letterCount = 64; static constexpr const char saltCharacters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "./0123456789"; // and trailing NUL static_assert(sizeof(saltCharacters) == (letterCount + 1), // 64 letters and trailing NUL "Salt-chars array is not exactly 64 letters long"); const quint32 index = QRandomGenerator::system()->bounded(0u, letterCount); return QLatin1Char(saltCharacters[index]); } #endif static QString saltPassword(const QString &plain) { #if HAVE_CRYPT_GENSALT QString salt = QString::fromLocal8Bit(crypt_gensalt(NULL, 0, NULL, 0)); #else QString salt; salt.append(u"$6$"); for (auto i = 0; i < 16; i++) { salt.append(saltCharacter()); } salt.append(u'$'); #endif auto stdStrPlain = plain.toStdString(); auto cStrPlain = stdStrPlain.c_str(); auto stdStrSalt = salt.toStdString(); auto cStrSalt = stdStrSalt.c_str(); auto salted = crypt(cStrPlain, cStrSalt); return QString::fromUtf8(salted); } void User::setPassword(const QString &password) { // Blocking because we need to wait for the password to be changed before we // can ask the user about also possibly changing their KWallet password auto message = m_dbusIface->SetPassword(saltPassword(password), QString()); message.waitForFinished(); // Not an error or invalid message if (message.isValid()) { Q_EMIT passwordSuccessfullyChanged(); } } QDBusObjectPath User::path() const { return mPath; } void User::apply() { const auto opt = [](bool cond, auto v) { if (cond) { return std::optional(v); } return std::optional(); }; auto job = new UserApplyJob(m_dbusIface, opt(mOriginalName != mName, mName), opt(mOriginalEmail != mEmail, mEmail), opt(mOriginalRealName != mRealName, mRealName), opt(mOriginalFace != mFace, mFace.toString(QUrl::PreferLocalFile).remove("file://"_L1)), opt(mOriginalAdministrator != mAdministrator, mAdministrator ? 1 : 0)); connect( job, &UserApplyJob::result, this, [this, job] { switch (static_cast(job->error())) { case UserApplyJob::Error::PermissionDenied: loadData(); // Reload the old data to avoid half transactions Q_EMIT applyError(i18n("Could not get permission to save user %1", mName)); break; case UserApplyJob::Error::Failed: case UserApplyJob::Error::Unknown: loadData(); // Reload the old data to avoid half transactions Q_EMIT applyError(i18n("There was an error while saving changes")); break; case UserApplyJob::Error::UserFacing: Q_EMIT applyError(job->errorText()); break; case UserApplyJob::Error::NoError:; // Do nothing } }, Qt::QueuedConnection /* result emission must be async, in the case of failure we'll manipulate states */); job->start(); } bool User::usesDefaultWallet() { const QStringList wallets = KWallet::Wallet::walletList(); return wallets.contains(QStringLiteral("kdewallet")); } void User::changeWalletPassword() { KWallet::Wallet::changePassword(QStringLiteral("kdewallet"), 1); } bool User::loggedIn() const { return mLoggedIn; } void User::setFaceCrop(const QRect &rect) { mFaceCrop = rect; } QRect User::faceCrop() const { return mFaceCrop; } UserApplyJob::UserApplyJob(QPointer dbusIface, std::optional name, std::optional email, std::optional realname, std::optional icon, std::optional type) : KJob() , m_name(name) , m_email(email) , m_realname(realname) , m_icon(icon) , m_type(type) , m_dbusIface(dbusIface) { } void UserApplyJob::start() { // With our UI the user expects the as a single transaction, but the accountsservice API does not provide that // When one of the writes fails, e.g. because the user cancelled the authentication dialog then none of the values should be applied // Not all calls trigger an authentication dialog, e.g. SetRealName for the current user does not but SetAccountType does // Therefore make a blocking call to SetAccountType first to trigger the auth dialog. If the user declines don't attempt to write anything else // This avoids settings any data when the user thinks they aborted the transaction, see https://bugs.kde.org/show_bug.cgi?id=425036 // Subsequent calls do not trigger the auth dialog again if (m_type.has_value()) { auto setAccount = m_dbusIface->SetAccountType(*m_type); setAccount.waitForFinished(); if (setAccount.isError()) { setError(setAccount.error()); qCWarning(KCMUSERS) << setAccount.error().name() << setAccount.error().message(); emitResult(); return; } } using UserPropertySetter = QDBusPendingReply<> (OrgFreedesktopAccountsUserInterface::*)(const QString &name); const std::array, UserPropertySetter>, 3> set = {{ {m_name, &OrgFreedesktopAccountsUserInterface::SetUserName}, {m_email, &OrgFreedesktopAccountsUserInterface::SetEmail}, {m_realname, &OrgFreedesktopAccountsUserInterface::SetRealName}, }}; for (auto const &[field, setter] : set) { if (!field.has_value()) { continue; } auto resp = (m_dbusIface->*setter)(field.value()); resp.waitForFinished(); if (resp.isError()) { setError(resp.error()); qCWarning(KCMUSERS) << resp.error().name() << resp.error().message(); emitResult(); return; } } // Icon is special, since we want to resize it. if (m_icon.has_value()) { QImage icon(*m_icon); // 256dp square is plenty big for an avatar and will definitely be smaller than 1MB QImage scaled = icon.scaled(QSize(256, 256), Qt::KeepAspectRatio, Qt::SmoothTransformation); QTemporaryFile file; if (!file.open()) { setErrorText(i18n("Failed to resize image: opening temp file failed")); qCWarning(KCMUSERS) << "Failed to resize image: opening temp file failed"; KJob::setError(static_cast(Error::UserFacing)); emitResult(); return; } if (!scaled.save(&file, "png")) { setErrorText(i18n("Failed to resize image: writing to temp file failed")); qCWarning(KCMUSERS) << "Failed to resize image: writing to temp file failed"; KJob::setError(static_cast(Error::UserFacing)); emitResult(); return; } file.close(); auto resp = m_dbusIface->SetIconFile(file.fileName()); resp.waitForFinished(); if (resp.isError()) { setError(resp.error()); qCWarning(KCMUSERS) << resp.error().name() << resp.error().message(); emitResult(); return; } } emitResult(); } void UserApplyJob::setError(const QDBusError &error) { setErrorText(error.message()); if (error.name() == QLatin1String("org.freedesktop.Accounts.Error.Failed")) { KJob::setError(static_cast(Error::Failed)); } else if (error.name() == QLatin1String("org.freedesktop.Accounts.Error.PermissionDenied")) { KJob::setError(static_cast(Error::PermissionDenied)); } else { KJob::setError(static_cast(Error::Unknown)); } }