feat(accounts): change the own password through PAM and ask for the current one

main
reionwong 2 weeks ago
parent 06cec04ae5
commit 6966c10a52

@ -8,6 +8,7 @@
#include "base/event_loop.h"
#include "base/paths.h"
#include "pages/accounts/password.h"
#include "pages/accounts/passwordchanger.h"
#include "pages/accounts/usercard.h"
#include "pages/accounts/userdialogs.h"
#include "ui/dialog_window.h"
@ -102,7 +103,11 @@ private:
m_chooser->open();
};
card->onChangePassword = [this, account] {
m_changePassword = std::make_unique<ChangePasswordDialog>(settingsWindow(), account->userName());
// The user's own goes through PAM, which asks for the current
// password and updates the login keyring with it.
const bool own = account->userId() == int64_t(getuid());
m_changePassword = std::make_unique<ChangePasswordDialog>(
settingsWindow(), account->userName(), own ? std::make_unique<PasswordChanger>() : nullptr);
m_changePassword->onPasswordSubmitted = [account](const std::string& password) {
account->setPassword(cryptPassword(password));
};

@ -54,37 +54,101 @@ protected:
}
};
ChangePasswordDialog::ChangePasswordDialog(SettingsWindow& parent, const std::string& userName)
ChangePasswordDialog::ChangePasswordDialog(SettingsWindow& parent, const std::string& userName,
std::unique_ptr<PasswordChanger> changer)
: FormDialog(parent, "Change password", kWidth)
, m_changer(std::move(changer))
{
Label* name = content()->addChild<SecondaryLabel>(userName);
name->setWrap(true);
name->setLayoutHints({.fillWidth = true});
GridLayout* grid = addForm(content());
m_password = addField(grid, "Password", true);
m_verify = addField(grid, "Verify password", true);
m_form = addForm(content());
if (m_changer) {
m_current = addField(m_form, "Current password", true);
m_password = addField(m_form, "New password", true);
} else {
m_password = addField(m_form, "Password", true);
}
m_verify = addField(m_form, "Verify password", true);
if (m_changer) {
m_error = content()->addChild<Label>();
m_error->setWrap(true);
m_error->setLayoutHints({.fillWidth = true});
m_error->setVisible(false);
m_changer->onFinished = [this](PasswordChanger::Result result, const std::string& message) {
switch (result) {
case PasswordChanger::Result::Changed:
close();
return;
case PasswordChanger::Result::WrongPassword:
m_current->setText({});
m_current->setFocus();
showError("The current password is incorrect");
break;
case PasswordChanger::Result::Rejected:
m_password->setText({});
m_verify->setText({});
m_password->setFocus();
showError(message.empty() ? "The new password was not accepted" : message);
break;
case PasswordChanger::Result::Failed:
showError(message.empty() ? "The password could not be changed" : message);
break;
case PasswordChanger::Result::TimedOut:
showError("The password change did not finish in time");
break;
}
update();
};
}
RowLayout* buttons = addButtonRow();
DialogButton* cancel = buttons->addChild<DialogButton>("Cancel");
cancel->onClicked = [this] { close(); };
m_cancel = buttons->addChild<DialogButton>("Cancel");
m_cancel->onClicked = [this] { requestClose(); };
m_change = buttons->addChild<DialogButton>("Change password", true);
m_change->onClicked = [this] {
m_change->onClicked = [this] { change(); };
for (TextField* field : {m_current, m_password, m_verify}) {
if (field)
field->onTextChanged = [this] { update(); };
}
canClose = [this] { return !m_changer || !m_changer->isRunning(); };
onShown = [this] { (m_current ? m_current : m_password)->setFocus(); };
update();
}
ChangePasswordDialog::~ChangePasswordDialog() = default;
void ChangePasswordDialog::update()
{
const bool waiting = m_changer && m_changer->isRunning();
m_form->setEnabled(!waiting);
m_cancel->setEnabled(!waiting);
m_change->setEnabled(!waiting && !m_password->text().empty() && m_password->text() == m_verify->text());
}
void ChangePasswordDialog::change()
{
if (!m_changer) {
const std::string password = m_password->text();
if (onPasswordSubmitted)
onPasswordSubmitted(password);
close();
};
return;
}
m_password->onTextChanged = [this] { update(); };
m_verify->onTextChanged = [this] { update(); };
onShown = [this] { m_password->setFocus(); };
showError({});
if (!m_changer->start(m_current->text(), m_password->text()))
showError("The password could not be changed");
update();
}
void ChangePasswordDialog::update()
void ChangePasswordDialog::showError(const std::string& error)
{
m_change->setEnabled(!m_password->text().empty() && m_password->text() == m_verify->text());
m_error->setText(error);
m_error->setVisible(!error.empty());
}
AddUserDialog::AddUserDialog(SettingsWindow& parent, accounts::AccountsManager& manager)

@ -4,8 +4,10 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include "pages/accounts/passwordchanger.h"
#include "widgets/formdialog.h"
namespace cutefish::accounts {
@ -22,20 +24,31 @@ class TextField;
namespace settings {
// A new password for an account, typed twice.
// A new password for an account, typed twice. With a changer it is the
// user's own: the current password is asked too, and the dialog stays open
// until the change is made, saying why it was not.
class ChangePasswordDialog : public FormDialog
{
public:
ChangePasswordDialog(SettingsWindow& parent, const std::string& userName);
ChangePasswordDialog(SettingsWindow& parent, const std::string& userName,
std::unique_ptr<PasswordChanger> changer = nullptr);
~ChangePasswordDialog() override;
// The password as typed; the caller hashes it.
// Without a changer: the password as typed; the caller hashes it.
std::function<void(const std::string& password)> onPasswordSubmitted;
private:
void update();
void change();
void showError(const std::string& error);
std::unique_ptr<PasswordChanger> m_changer;
cutefish::ui::GridLayout* m_form;
cutefish::ui::TextField* m_current = nullptr;
cutefish::ui::TextField* m_password;
cutefish::ui::TextField* m_verify;
cutefish::ui::Label* m_error = nullptr;
cutefish::ui::DialogButton* m_cancel;
cutefish::ui::DialogButton* m_change;
};

@ -13,9 +13,17 @@
#include <sys/stat.h>
#include "pages/accounts/passwordchanger.h"
#include "pages/accounts/userdialogs.h"
#include "ui/dialog_window.h"
#include "ui/label.h"
#include "ui/test_input.h"
#include "ui/text_field.h"
#include "ui/window.h"
#include "window/settingswindow.h"
#include "input_test_support.h"
using namespace cutefish;
using namespace cutefish::ui;
using namespace settings;
using namespace std::chrono_literals;
@ -117,6 +125,40 @@ std::optional<Outcome> change(const std::string& program, const std::string& cur
return outcome;
}
template<typename T>
std::vector<T*> findAll(View* view)
{
std::vector<T*> found;
if (auto* match = dynamic_cast<T*>(view))
found.push_back(match);
for (const auto& child : view->children()) {
for (T* match : findAll<T>(child.get()))
found.push_back(match);
}
return found;
}
PointF centre(View* view)
{
const PointF origin = view->mapToWindow({0, 0});
return {origin.x + view->geometry().width / 2, origin.y + view->geometry().height / 2};
}
void typeInto(Window& window, TextField* field, const std::string& text)
{
TestInput::click(window, centre(field));
TestInput::typeText(window, text);
}
bool showsText(Window& window, const std::string& text)
{
for (Label* label : findAll<Label>(window.contentView())) {
if (label->isVisible() && label->text() == text)
return true;
}
return false;
}
} // namespace
TEST(AccountsPassword, ChangesThroughPasswdsPrompts)
@ -169,3 +211,39 @@ TEST(AccountsPassword, AMissingPasswdDoesNotStart)
EXPECT_FALSE(changer.start("old", "new password"));
EXPECT_FALSE(changer.isRunning());
}
TEST(AccountsPassword, TheOwnAccountsDialogAsksTheCurrentPassword)
{
FakePasswd passwd(kPasswd);
SettingsWindow window;
ChangePasswordDialog dialog(window, "reion", std::make_unique<PasswordChanger>(passwd.path));
dialog.show();
Window& dialogWindow = *dialog.window();
dialogWindow.grab();
const auto fields = findAll<TextField>(dialogWindow.contentView());
ASSERT_EQ(fields.size(), 3u);
for (TextField* field : fields)
EXPECT_EQ(field->echoMode(), TextField::EchoMode::Password);
const auto buttons = findAll<DialogButton>(dialogWindow.contentView());
ASSERT_EQ(buttons.size(), 2u);
DialogButton* change = buttons[1];
typeInto(dialogWindow, fields[0], "wrong");
typeInto(dialogWindow, fields[1], "new password");
typeInto(dialogWindow, fields[2], "new password");
EXPECT_TRUE(change->isEnabled());
dialogWindow.grab();
TestInput::click(dialogWindow, centre(change));
EXPECT_FALSE(change->isEnabled());
ASSERT_TRUE(settings::testing::waitFor([&] { return change->isEnabled(); }, 5000ms));
EXPECT_TRUE(dialog.isOpen());
EXPECT_TRUE(showsText(dialogWindow, "The current password is incorrect"));
EXPECT_EQ(fields[0]->text(), "");
typeInto(dialogWindow, fields[0], "old");
dialogWindow.grab();
TestInput::click(dialogWindow, centre(change));
ASSERT_TRUE(settings::testing::waitFor([&] { return !dialog.isOpen(); }, 5000ms));
EXPECT_EQ(passwd.changed(), "new password");
}

Loading…
Cancel
Save