From ab67b7d2f15a764bc5d4b0104d52a6568faa7618 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 18 Sep 2017 16:08:21 +0200 Subject: [PATCH] Passwords: introduce password-checking - Introduce a map 'passwordRequirements' in users.conf, which is a list of named requirements. There are only two settings right now, min and max length, but additional checks can easily be added in UsersPage.cpp by defining additional lambda's to check the given password string. - Add PasswordCheck instances as needed, with functions to check acceptability and to produce messages on rejection. - Documentation in the users.conf file itself. - In passing, refactor setting of pixmaps on labels. FIXES #790 --- src/modules/users/UsersPage.cpp | 159 +++++++++++++++++++++------- src/modules/users/UsersPage.h | 43 +++++++- src/modules/users/UsersViewStep.cpp | 15 ++- src/modules/users/users.conf | 14 +++ 4 files changed, 189 insertions(+), 42 deletions(-) diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 87e64deb1..7c4bb6a7d 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -37,7 +37,12 @@ #include #include - +/** Add a standard pixmap to a label. */ +static void +markLabel( QLabel* label, CalamaresUtils::ImageType i ) +{ + label->setPixmap( CalamaresUtils::defaultPixmap( i, CalamaresUtils::Original, label->size() ) ); +} UsersPage::UsersPage( QWidget* parent ) : QWidget( parent ) @@ -268,9 +273,7 @@ UsersPage::validateUsernameText( const QString& textRef ) } else if ( text.length() > USERNAME_MAX_LENGTH ) { - ui->labelUsername->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::No, - CalamaresUtils::Original, - ui->labelUsername->size() ) ); + markLabel( ui->labelUsername, CalamaresUtils::No ); ui->labelUsernameError->setText( tr( "Your username is too long." ) ); @@ -278,18 +281,14 @@ UsersPage::validateUsernameText( const QString& textRef ) } else if ( val.validate( text, pos ) == QValidator::Invalid ) { - ui->labelUsername->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::No, - CalamaresUtils::Original, - ui->labelUsername->size() ) ); + markLabel( ui->labelUsername, CalamaresUtils::No ); ui->labelUsernameError->setText( tr( "Your username contains invalid characters. Only lowercase letters and numbers are allowed." ) ); m_readyUsername = false; } else { - ui->labelUsername->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, - CalamaresUtils::Original, - ui->labelUsername->size() ) ); + markLabel( ui->labelUsername, CalamaresUtils::Yes ); ui->labelUsernameError->clear(); m_readyUsername = true; } @@ -322,9 +321,7 @@ UsersPage::validateHostnameText( const QString& textRef ) } else if ( text.length() < HOSTNAME_MIN_LENGTH ) { - ui->labelHostname->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::No, - CalamaresUtils::Original, - ui->labelHostname->size() ) ); + markLabel( ui->labelHostname, CalamaresUtils::No ); ui->labelHostnameError->setText( tr( "Your hostname is too short." ) ); @@ -333,9 +330,7 @@ UsersPage::validateHostnameText( const QString& textRef ) } else if ( text.length() > HOSTNAME_MAX_LENGTH ) { - ui->labelHostname->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::No, - CalamaresUtils::Original, - ui->labelHostname->size() ) ); + markLabel( ui->labelHostname, CalamaresUtils::No ); ui->labelHostnameError->setText( tr( "Your hostname is too long." ) ); @@ -344,9 +339,7 @@ UsersPage::validateHostnameText( const QString& textRef ) } else if ( val.validate( text, pos ) == QValidator::Invalid ) { - ui->labelHostname->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::No, - CalamaresUtils::Original, - ui->labelHostname->size() ) ); + markLabel( ui->labelHostname, CalamaresUtils::No ); ui->labelHostnameError->setText( tr( "Your hostname contains invalid characters. Only letters, numbers and dashes are allowed." ) ); @@ -354,9 +347,7 @@ UsersPage::validateHostnameText( const QString& textRef ) } else { - ui->labelHostname->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, - CalamaresUtils::Original, - ui->labelHostname->size() ) ); + markLabel( ui->labelHostname, CalamaresUtils::Yes ); ui->labelHostnameError->clear(); m_readyHostname = true; } @@ -364,7 +355,6 @@ UsersPage::validateHostnameText( const QString& textRef ) emit checkReady( isReady() ); } - void UsersPage::onPasswordTextChanged( const QString& ) { @@ -380,24 +370,35 @@ UsersPage::onPasswordTextChanged( const QString& ) else if ( pw1 != pw2 ) { ui->labelUserPasswordError->setText( tr( "Your passwords do not match!" ) ); - ui->labelUserPassword->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::No, - CalamaresUtils::Original, - ui->labelUserPassword->size() ) ); + markLabel( ui->labelUserPassword, CalamaresUtils::No ); m_readyPassword = false; } else { - ui->labelUserPasswordError->clear(); - ui->labelUserPassword->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, - CalamaresUtils::Original, - ui->labelUserPassword->size() ) ); - m_readyPassword = true; + bool ok = true; + for ( auto pc : m_passwordChecks ) + { + QString s = pc.filter( pw1 ); + if ( !s.isEmpty() ) + { + ui->labelUserPasswordError->setText( s ); + markLabel( ui->labelUserPassword, CalamaresUtils::No ); + ok = false; + m_readyPassword = false; + } + } + + if ( ok ) + { + ui->labelUserPasswordError->clear(); + markLabel( ui->labelUserPassword, CalamaresUtils::Yes ); + m_readyPassword = true; + } } emit checkReady( isReady() ); } - void UsersPage::onRootPasswordTextChanged( const QString& ) { @@ -413,18 +414,30 @@ UsersPage::onRootPasswordTextChanged( const QString& ) else if ( pw1 != pw2 ) { ui->labelRootPasswordError->setText( tr( "Your passwords do not match!" ) ); - ui->labelRootPassword->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::No, - CalamaresUtils::Original, - ui->labelRootPassword->size() ) ); + markLabel( ui->labelRootPassword, CalamaresUtils::No ); m_readyRootPassword = false; } else { - ui->labelRootPasswordError->clear(); - ui->labelRootPassword->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, - CalamaresUtils::Original, - ui->labelRootPassword->size() ) ); - m_readyRootPassword = true; + bool ok = true; + for ( auto pc : m_passwordChecks ) + { + QString s = pc.filter( pw1 ); + if ( !s.isEmpty() ) + { + ui->labelRootPasswordError->setText( s ); + markLabel( ui->labelRootPassword, CalamaresUtils::No ); + ok = false; + m_readyRootPassword = false; + } + } + + if ( ok ) + { + ui->labelRootPasswordError->clear(); + markLabel( ui->labelRootPassword, CalamaresUtils::Yes ); + m_readyRootPassword = true; + } } emit checkReady( isReady() ); @@ -444,3 +457,69 @@ UsersPage::setReusePasswordDefault( bool checked ) ui->checkBoxReusePassword->setChecked( checked ); emit checkReady( isReady() ); } + +UsersPage::PasswordCheck::PasswordCheck() + : m_message() + , m_accept( []( const QString& s ) +{ + return true; +} ) +{ +} + +UsersPage::PasswordCheck::PasswordCheck( const QString& m, AcceptFunc a ) + : m_message( [m](){ return m; } ) + , m_accept( a ) +{ +} + +UsersPage::PasswordCheck::PasswordCheck( MessageFunc m, AcceptFunc a ) + : m_message( m ) + , m_accept( a ) +{ +} + +void +UsersPage::addPasswordCheck( const QString& key, const QVariant& value ) +{ + if ( key == "minLength" ) + { + int minLength = -1; + if ( value.canConvert( QVariant::Int ) ) + minLength = value.toInt(); + if ( minLength > 0 ) + { + cDebug() << key << " .. set to" << minLength; + m_passwordChecks.push_back( + PasswordCheck( + []() + { + return tr( "Password is too short" ); + }, + [minLength]( const QString& s ) + { + return s.length() >= minLength; + } ) ); + } + } + else if ( key == "maxLength" ) + { + int maxLength = -1; + if ( value.canConvert( QVariant::Int ) ) + maxLength = value.toInt(); + if ( maxLength > 0 ) + { + cDebug() << key << " .. set to" << maxLength; + m_passwordChecks.push_back( + PasswordCheck( []() + { + return tr( "Password is too long" ); + }, [maxLength]( const QString& s ) + { + return s.length() <= maxLength; + } ) ); + } + } + else + cDebug() << "WARNING: Unknown password-check key" << '"' << key << '"'; +} diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index 0f328f46c..5a72e11de 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -28,7 +28,10 @@ #include -namespace Ui { +#include + +namespace Ui +{ class Page_UserSetup; } @@ -49,6 +52,8 @@ public: void setAutologinDefault( bool checked ); void setReusePasswordDefault( bool checked ); + void addPasswordCheck( const QString& key, const QVariant& value ); + protected slots: void onFullNameTextEdited( const QString& ); void fillSuggestions(); @@ -65,6 +70,42 @@ signals: private: Ui::Page_UserSetup* ui; + /** + * Support for (dynamic) checks on the password's validity. + * This can be used to implement password requirements like + * "at least 6 characters". Function addPasswordCheck() + * instantiates these and adds them to the list of checks. + */ + class PasswordCheck + { + public: + /** Return true if the string is acceptable. */ + using AcceptFunc = std::function; + using MessageFunc = std::function; + + /** Generate a @p message if @p filter returns true */ + PasswordCheck( MessageFunc message, AcceptFunc filter ); + /** Yields @p message if @p filter returns true */ + PasswordCheck( const QString& message, AcceptFunc filter ); + /** Null check, always returns empty */ + PasswordCheck(); + + /** Applies this check to the given password string @p s + * and returns an empty string if the password is ok + * according to this filter. Returns a message describing + * what is wrong if not. + */ + QString filter( const QString& s ) const + { + return m_accept( s ) ? QString() : m_message(); + } + + private: + MessageFunc m_message; + AcceptFunc m_accept; + } ; + QVector m_passwordChecks; + const QRegExp USERNAME_RX = QRegExp( "^[a-z_][a-z0-9_-]*[$]?$" ); const QRegExp HOSTNAME_RX = QRegExp( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); const int USERNAME_MAX_LENGTH = 31; diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index d601014ae..25b4dee84 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -20,6 +20,7 @@ #include "UsersPage.h" +#include "utils/Logger.h" #include "JobQueue.h" #include "GlobalStorage.h" @@ -159,11 +160,23 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { m_widget->setAutologinDefault( configurationMap.value( "doAutologin" ).toBool() ); } - + if ( configurationMap.contains( "doReusePassword" ) && configurationMap.value( "doReusePassword" ).type() == QVariant::Bool ) { m_widget->setReusePasswordDefault( configurationMap.value( "doReusePassword" ).toBool() ); } + + if ( configurationMap.contains( "passwordRequirements" ) && + configurationMap.value( "passwordRequirements" ).type() == QVariant::Map ) + { + auto pr_checks( configurationMap.value( "passwordRequirements" ).toMap() ); + + for (decltype(pr_checks)::const_iterator i = pr_checks.constBegin(); + i != pr_checks.constEnd(); ++i) + { + m_widget->addPasswordCheck( i.key(), i.value() ); + } + } } diff --git a/src/modules/users/users.conf b/src/modules/users/users.conf index dea0ba999..d5466c62f 100644 --- a/src/modules/users/users.conf +++ b/src/modules/users/users.conf @@ -27,3 +27,17 @@ sudoersGroup: wheel setRootPassword: true doReusePassword: true + +# These are optional password-requirements that a distro can enforce +# on the user. The values given in this sample file disable each check, +# as if the check was not listed at all. +# +# Checks may be listed multiple times; each is checked separately, +# and no effort is done to ensure that the checks are consistent +# (e.g. specifying a maximum length less than the minimum length +# will annoy users). +# +# (additional checks may be implemented in UsersPage.cpp) +passwordRequirements: + minLength: -1 # Password at least this many characters + maxLength: -1 # Password at most this many characters