diff --git a/CHANGES b/CHANGES index e5e3c42ba..aca4d6d5e 100644 --- a/CHANGES +++ b/CHANGES @@ -6,13 +6,22 @@ website will have to do for older versions. # 3.2.16 (unreleased) # This release contains contributions from (alphabetically by first name): - - No other contributors this time around. + - Bill Auger ## Core ## - - No changes to core functionality + - Some obscure build scenarios which would lead to bogus module-is- + misconfigured messages on startup have been resolved. ## Modules ## - - No changes to module functionality + - The explanatory messages on the *users* page have moved to tooltips, + and placeholder text has been added to the fields. #1202 + - The bad-password messages in the *users* page have been improved. #1261 + - Password-checking in the *users* module has been substantially + changed. A new key *allowWeakPasswords* can be used to introduce + an additional checkbox to the page, which can then be used to + switch off strict password checking. (Thanks to Bill Auger) + - The icons used in password warnings on the *users* page have been + changed to the colorful status icons (rather than the thin red X). # 3.2.15 (2019-10-11) # diff --git a/src/libcalamaresui/ViewManager.cpp b/src/libcalamaresui/ViewManager.cpp index 06fd97009..68d918971 100644 --- a/src/libcalamaresui/ViewManager.cpp +++ b/src/libcalamaresui/ViewManager.cpp @@ -75,26 +75,6 @@ setButtonIcon( QPushButton* button, const QString& name ) } } -/** @brief Creates a button with a given icon-name - * - * Creates a new button as child of @p parent. - * Sets the named icon, if it exists, onto the button. - * Returns the new button. - * - * There is a QPushButton constructor that takes an icon, - * but it also needs a text and we've got translations - * to worry about as well as state. - */ -static inline QPushButton* -makeButton( QWidget* parent, const QString& name, const QString& label ) -{ - QPushButton* button = new QPushButton( parent ); - button->setObjectName( name ); - button->setText( label ); - setButtonIcon( button, name ); - return button; -} - ViewManager::ViewManager( QObject* parent ) : QObject( parent ) , m_currentStep( 0 ) @@ -110,9 +90,12 @@ ViewManager::ViewManager( QObject* parent ) mainLayout->addWidget( m_stack ); // Create buttons and sets an initial icon; the icons may change - m_back = makeButton( m_widget, QStringLiteral( "go-previous" ), tr( "&Back" ) ); - m_next = makeButton( m_widget, QStringLiteral( "go-next" ), tr( "&Next" ) ); - m_quit = makeButton( m_widget, QStringLiteral( "dialog-cancel" ), tr( "&Cancel" ) ); + m_back = new QPushButton( getButtonIcon( QStringLiteral( "go-previous" ) ), tr( "&Back" ), m_widget ); + m_back->setObjectName( "view-button-back" ); + m_next = new QPushButton( getButtonIcon( QStringLiteral( "go-next" ) ), tr( "&Next" ), m_widget ); + m_next->setObjectName( "view-button-next" ); + m_quit = new QPushButton( getButtonIcon( QStringLiteral( "dialog-cancel" ) ), tr( "&Cancel" ), m_widget ); + m_quit->setObjectName( "view-button-cancel" ); CALAMARES_RETRANSLATE_SLOT( &ViewManager::updateButtonLabels ) diff --git a/src/modules/users/CheckPWQuality.cpp b/src/modules/users/CheckPWQuality.cpp index 7fa13f124..3728c5a92 100644 --- a/src/modules/users/CheckPWQuality.cpp +++ b/src/modules/users/CheckPWQuality.cpp @@ -32,12 +32,12 @@ PasswordCheck::PasswordCheck() : m_message() - , m_accept( []( const QString& ){ return true; } ) + , m_accept( []( const QString& ) { return true; } ) { } PasswordCheck::PasswordCheck( const QString& m, AcceptFunc a ) - : m_message( [m](){ return m; } ) + : m_message( [m]() { return m; } ) , m_accept( a ) { } @@ -52,21 +52,14 @@ DEFINE_CHECK_FUNC( minLength ) { int minLength = -1; if ( value.canConvert( QVariant::Int ) ) + { minLength = value.toInt(); + } if ( minLength > 0 ) { cDebug() << Logger::SubEntry << "minLength set to" << minLength; - checks.push_back( - PasswordCheck( - []() - { - return QCoreApplication::translate( "PWQ", "Password is too short" ); - }, - [minLength]( const QString& s ) - { - return s.length() >= minLength; - } - ) ); + checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too short" ); }, + [minLength]( const QString& s ) { return s.length() >= minLength; } ) ); } } @@ -74,21 +67,14 @@ DEFINE_CHECK_FUNC( maxLength ) { int maxLength = -1; if ( value.canConvert( QVariant::Int ) ) + { maxLength = value.toInt(); + } if ( maxLength > 0 ) { cDebug() << Logger::SubEntry << "maxLength set to" << maxLength; - checks.push_back( - PasswordCheck( - []() - { - return QCoreApplication::translate("PWQ", "Password is too long" ); - }, - [maxLength]( const QString& s ) - { - return s.length() <= maxLength; - } - ) ); + checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too long" ); }, + [maxLength]( const QString& s ) { return s.length() <= maxLength; } ) ); } } @@ -101,15 +87,17 @@ DEFINE_CHECK_FUNC( maxLength ) */ /// @brief Handle libpwquality using void* to represent a long -static inline long mungeLong( void* p ) +static inline long +mungeLong( void* p ) { - return static_cast( reinterpret_cast( p ) ); + return static_cast< long >( reinterpret_cast< intptr_t >( p ) ); } /// @brief Handle libpwquality using void* to represent a char* -static inline const char* mungeString( void* p ) +static inline const char* +mungeString( void* p ) { - return reinterpret_cast( p ); + return reinterpret_cast< const char* >( p ); } /** @@ -128,16 +116,10 @@ public: { } - ~PWSettingsHolder() - { - pwquality_free_settings( m_settings ); - } + ~PWSettingsHolder() { pwquality_free_settings( m_settings ); } /// Sets an option via the configuration string @p v, = style. - int set( const QString& v ) - { - return pwquality_set_option( m_settings, v.toUtf8().constData() ); - } + int set( const QString& v ) { return pwquality_set_option( m_settings, v.toUtf8().constData() ); } /// Checks the given password @p pwd against the current configuration int check( const QString& pwd ) @@ -148,10 +130,7 @@ public: return r; } - bool hasExplanation() const - { - return m_rv < 0; - } + bool hasExplanation() const { return m_rv < 0; } /* This is roughly the same as the function pwquality_strerror, * only with QStrings instead, and using the Qt translation scheme. @@ -164,16 +143,21 @@ public: m_auxerror = nullptr; if ( m_rv >= arbitrary_minimum_strength ) + { return QString(); + } if ( m_rv >= 0 ) - return QCoreApplication::translate( "PWQ", "Password is too weak" ); + { + return QCoreApplication::translate( "PWQ", "Password is too weak" ); + } switch ( m_rv ) { case PWQ_ERROR_MEM_ALLOC: if ( auxerror ) { - QString s = QCoreApplication::translate( "PWQ", "Memory allocation error when setting '%1'" ).arg( mungeString( auxerror ) ); + QString s = QCoreApplication::translate( "PWQ", "Memory allocation error when setting '%1'" ) + .arg( mungeString( auxerror ) ); free( auxerror ); return s; } @@ -189,58 +173,94 @@ public: case PWQ_ERROR_USER_CHECK: return QCoreApplication::translate( "PWQ", "The password contains the user name in some form" ); case PWQ_ERROR_GECOS_CHECK: - return QCoreApplication::translate( "PWQ", "The password contains words from the real name of the user in some form" ); + return QCoreApplication::translate( + "PWQ", "The password contains words from the real name of the user in some form" ); case PWQ_ERROR_BAD_WORDS: return QCoreApplication::translate( "PWQ", "The password contains forbidden words in some form" ); case PWQ_ERROR_MIN_DIGITS: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password contains less than %1 digits" ).arg( mungeLong( auxerror ) ); + { + return QCoreApplication::translate( "PWQ", "The password contains less than %1 digits" ) + .arg( mungeLong( auxerror ) ); + } return QCoreApplication::translate( "PWQ", "The password contains too few digits" ); case PWQ_ERROR_MIN_UPPERS: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password contains less than %1 uppercase letters" ).arg( mungeLong( auxerror ) ); + { + return QCoreApplication::translate( "PWQ", "The password contains less than %1 uppercase letters" ) + .arg( mungeLong( auxerror ) ); + } return QCoreApplication::translate( "PWQ", "The password contains too few uppercase letters" ); case PWQ_ERROR_MIN_LOWERS: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password contains less than %1 lowercase letters" ).arg( mungeLong( auxerror ) ); + { + return QCoreApplication::translate( "PWQ", "The password contains less than %1 lowercase letters" ) + .arg( mungeLong( auxerror ) ); + } return QCoreApplication::translate( "PWQ", "The password contains too few lowercase letters" ); case PWQ_ERROR_MIN_OTHERS: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password contains less than %1 non-alphanumeric characters" ).arg( mungeLong( auxerror ) ); + { + return QCoreApplication::translate( "PWQ", + "The password contains less than %1 non-alphanumeric characters" ) + .arg( mungeLong( auxerror ) ); + } return QCoreApplication::translate( "PWQ", "The password contains too few non-alphanumeric characters" ); case PWQ_ERROR_MIN_LENGTH: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password is shorter than %1 characters" ).arg( mungeLong( auxerror ) ); + { + return QCoreApplication::translate( "PWQ", "The password is shorter than %1 characters" ) + .arg( mungeLong( auxerror ) ); + } return QCoreApplication::translate( "PWQ", "The password is too short" ); case PWQ_ERROR_ROTATED: return QCoreApplication::translate( "PWQ", "The password is just rotated old one" ); case PWQ_ERROR_MIN_CLASSES: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password contains less than %1 character classes" ).arg( mungeLong( auxerror ) ); + { + return QCoreApplication::translate( "PWQ", "The password contains less than %1 character classes" ) + .arg( mungeLong( auxerror ) ); + } return QCoreApplication::translate( "PWQ", "The password does not contain enough character classes" ); case PWQ_ERROR_MAX_CONSECUTIVE: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password contains more than %1 same characters consecutively" ).arg( mungeLong( auxerror ) ); + { + return QCoreApplication::translate( "PWQ", + "The password contains more than %1 same characters consecutively" ) + .arg( mungeLong( auxerror ) ); + } return QCoreApplication::translate( "PWQ", "The password contains too many same characters consecutively" ); case PWQ_ERROR_MAX_CLASS_REPEAT: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password contains more than %1 characters of the same class consecutively" ).arg( mungeLong( auxerror ) ); - return QCoreApplication::translate( "PWQ", "The password contains too many characters of the same class consecutively" ); + { + return QCoreApplication::translate( + "PWQ", "The password contains more than %1 characters of the same class consecutively" ) + .arg( mungeLong( auxerror ) ); + } + return QCoreApplication::translate( + "PWQ", "The password contains too many characters of the same class consecutively" ); case PWQ_ERROR_MAX_SEQUENCE: if ( auxerror ) - return QCoreApplication::translate( "PWQ", "The password contains monotonic sequence longer than %1 characters" ).arg( mungeLong( auxerror ) ); - return QCoreApplication::translate( "PWQ", "The password contains too long of a monotonic character sequence" ); + { + return QCoreApplication::translate( + "PWQ", "The password contains monotonic sequence longer than %1 characters" ) + .arg( mungeLong( auxerror ) ); + } + return QCoreApplication::translate( "PWQ", + "The password contains too long of a monotonic character sequence" ); case PWQ_ERROR_EMPTY_PASSWORD: return QCoreApplication::translate( "PWQ", "No password supplied" ); case PWQ_ERROR_RNG: return QCoreApplication::translate( "PWQ", "Cannot obtain random numbers from the RNG device" ); case PWQ_ERROR_GENERATION_FAILED: - return QCoreApplication::translate( "PWQ", "Password generation failed - required entropy too low for settings" ); + return QCoreApplication::translate( "PWQ", + "Password generation failed - required entropy too low for settings" ); case PWQ_ERROR_CRACKLIB_CHECK: if ( auxerror ) { /* Here the string comes from cracklib, don't free? */ - return QCoreApplication::translate( "PWQ", "The password fails the dictionary check - %1" ).arg( mungeString( auxerror ) ); + return QCoreApplication::translate( "PWQ", "The password fails the dictionary check - %1" ) + .arg( mungeString( auxerror ) ); } return QCoreApplication::translate( "PWQ", "The password fails the dictionary check" ); case PWQ_ERROR_UNKNOWN_SETTING: @@ -254,7 +274,8 @@ public: case PWQ_ERROR_INTEGER: if ( auxerror ) { - QString s = QCoreApplication::translate( "PWQ", "Bad integer value of setting - %1" ).arg( mungeString( auxerror ) ); + QString s = QCoreApplication::translate( "PWQ", "Bad integer value of setting - %1" ) + .arg( mungeString( auxerror ) ); free( auxerror ); return s; } @@ -262,7 +283,8 @@ public: case PWQ_ERROR_NON_INT_SETTING: if ( auxerror ) { - QString s = QCoreApplication::translate( "PWQ", "Setting %1 is not of integer type" ).arg( mungeString( auxerror ) ); + QString s = QCoreApplication::translate( "PWQ", "Setting %1 is not of integer type" ) + .arg( mungeString( auxerror ) ); free( auxerror ); return s; } @@ -270,7 +292,8 @@ public: case PWQ_ERROR_NON_STR_SETTING: if ( auxerror ) { - QString s = QCoreApplication::translate( "PWQ", "Setting %1 is not of string type" ).arg( mungeString( auxerror ) ); + QString s = QCoreApplication::translate( "PWQ", "Setting %1 is not of string type" ) + .arg( mungeString( auxerror ) ); free( auxerror ); return s; } @@ -290,7 +313,7 @@ private: pwquality_settings_t* m_settings; int m_rv; void* m_auxerror; -} ; +}; DEFINE_CHECK_FUNC( libpwquality ) { @@ -302,7 +325,7 @@ DEFINE_CHECK_FUNC( libpwquality ) QVariantList l = value.toList(); unsigned int requirement_count = 0; - auto settings = std::make_shared(); + auto settings = std::make_shared< PWSettingsHolder >(); for ( const auto& v : l ) { if ( v.type() == QVariant::String ) @@ -310,7 +333,9 @@ DEFINE_CHECK_FUNC( libpwquality ) QString option = v.toString(); int r = settings->set( option ); if ( r ) + { cWarning() << "unrecognized libpwquality setting" << option; + } else { cDebug() << Logger::SubEntry << "libpwquality setting" << option; @@ -318,28 +343,27 @@ DEFINE_CHECK_FUNC( libpwquality ) } } else + { cWarning() << "unrecognized libpwquality setting" << v; + } } /* Something actually added? */ if ( requirement_count ) { - checks.push_back( - PasswordCheck( - [settings]() - { - return settings->explanation(); - }, - [settings]( const QString& s ) - { - int r = settings->check( s ); - if ( r < 0 ) - cWarning() << "libpwquality error" << r; - else if ( r < settings->arbitrary_minimum_strength ) - cDebug() << "Password strength" << r << "too low"; - return r >= settings->arbitrary_minimum_strength; - } - ) ); + checks.push_back( PasswordCheck( [settings]() { return settings->explanation(); }, + [settings]( const QString& s ) { + int r = settings->check( s ); + if ( r < 0 ) + { + cWarning() << "libpwquality error" << r; + } + else if ( r < settings->arbitrary_minimum_strength ) + { + cDebug() << "Password strength" << r << "too low"; + } + return r >= settings->arbitrary_minimum_strength; + } ) ); } } #endif diff --git a/src/modules/users/CheckPWQuality.h b/src/modules/users/CheckPWQuality.h index 07760c75b..046f31496 100644 --- a/src/modules/users/CheckPWQuality.h +++ b/src/modules/users/CheckPWQuality.h @@ -35,8 +35,8 @@ class PasswordCheck { public: /** Return true if the string is acceptable. */ - using AcceptFunc = std::function; - using MessageFunc = std::function; + using AcceptFunc = std::function< bool( const QString& ) >; + using MessageFunc = std::function< QString() >; /** Generate a @p message if @p filter returns true */ PasswordCheck( MessageFunc message, AcceptFunc filter ); @@ -50,17 +50,14 @@ public: * 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(); - } + QString filter( const QString& s ) const { return m_accept( s ) ? QString() : m_message(); } private: MessageFunc m_message; AcceptFunc m_accept; -} ; +}; -using PasswordCheckList = QVector; +using PasswordCheckList = QVector< PasswordCheck >; /* Each of these functions adds a check (if possible) to the list * of checks; they use the configuration value(s) from the @@ -68,16 +65,14 @@ using PasswordCheckList = QVector; * may skip adding a check, and do nothing (it should log * an error, though). */ -#define _xDEFINE_CHECK_FUNC(x) \ - add_check_##x( PasswordCheckList& checks, const QVariant& value ) -#define DEFINE_CHECK_FUNC(x) void _xDEFINE_CHECK_FUNC(x) -#define DECLARE_CHECK_FUNC(x) void _xDEFINE_CHECK_FUNC(x); +#define _xDEFINE_CHECK_FUNC( x ) add_check_##x( PasswordCheckList& checks, const QVariant& value ) +#define DEFINE_CHECK_FUNC( x ) void _xDEFINE_CHECK_FUNC( x ) +#define DECLARE_CHECK_FUNC( x ) void _xDEFINE_CHECK_FUNC( x ); -DECLARE_CHECK_FUNC(minLength) -DECLARE_CHECK_FUNC(maxLength) +DECLARE_CHECK_FUNC( minLength ) +DECLARE_CHECK_FUNC( maxLength ) #ifdef HAVE_LIBPWQUALITY -DECLARE_CHECK_FUNC(libpwquality) +DECLARE_CHECK_FUNC( libpwquality ) #endif #endif - diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index 788ba0195..b107fb0bb 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -19,10 +19,10 @@ #include -#include "JobQueue.h" #include "GlobalStorage.h" -#include "utils/Logger.h" +#include "JobQueue.h" #include "utils/CalamaresUtilsSystem.h" +#include "utils/Logger.h" #include #include @@ -72,17 +72,20 @@ CreateUserJob::exec() Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); QDir destDir( gs->value( "rootMountPoint" ).toString() ); - if ( gs->contains( "sudoersGroup" ) && - !gs->value( "sudoersGroup" ).toString().isEmpty() ) + if ( gs->contains( "sudoersGroup" ) && !gs->value( "sudoersGroup" ).toString().isEmpty() ) { QFileInfo sudoersFi( destDir.absoluteFilePath( "etc/sudoers.d/10-installer" ) ); if ( !sudoersFi.absoluteDir().exists() ) + { return Calamares::JobResult::error( tr( "Sudoers dir is not writable." ) ); + } QFile sudoersFile( sudoersFi.absoluteFilePath() ); - if (!sudoersFile.open( QIODevice::WriteOnly | QIODevice::Text ) ) + if ( !sudoersFile.open( QIODevice::WriteOnly | QIODevice::Text ) ) + { return Calamares::JobResult::error( tr( "Cannot create sudoers file for writing." ) ); + } QString sudoersGroup = gs->value( "sudoersGroup" ).toString(); @@ -96,11 +99,12 @@ CreateUserJob::exec() QFileInfo groupsFi( destDir.absoluteFilePath( "etc/group" ) ); QFile groupsFile( groupsFi.absoluteFilePath() ); if ( !groupsFile.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { return Calamares::JobResult::error( tr( "Cannot open groups file for reading." ) ); + } QString groupsData = QString::fromLocal8Bit( groupsFile.readAll() ); QStringList groupsLines = groupsData.split( '\n' ); - for ( QStringList::iterator it = groupsLines.begin(); - it != groupsLines.end(); ++it ) + for ( QStringList::iterator it = groupsLines.begin(); it != groupsLines.end(); ++it ) { int indexOfFirstToDrop = it->indexOf( ':' ); it->truncate( indexOfFirstToDrop ); @@ -108,15 +112,13 @@ CreateUserJob::exec() foreach ( const QString& group, m_defaultGroups ) if ( !groupsLines.contains( group ) ) - CalamaresUtils::System::instance()-> - targetEnvCall( { "groupadd", group } ); + CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", group } ); QString defaultGroups = m_defaultGroups.join( ',' ); if ( m_autologin ) { QString autologinGroup; - if ( gs->contains( "autologinGroup" ) && - !gs->value( "autologinGroup" ).toString().isEmpty() ) + if ( gs->contains( "autologinGroup" ) && !gs->value( "autologinGroup" ).toString().isEmpty() ) { autologinGroup = gs->value( "autologinGroup" ).toString(); CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", autologinGroup } ); @@ -131,26 +133,20 @@ CreateUserJob::exec() QDir existingHome( destDir.absolutePath() + shellFriendlyHome ); if ( existingHome.exists() ) { - QString backupDirName = "dotfiles_backup_" + - QDateTime::currentDateTime() - .toString( "yyyy-MM-dd_HH-mm-ss" ); + QString backupDirName = "dotfiles_backup_" + QDateTime::currentDateTime().toString( "yyyy-MM-dd_HH-mm-ss" ); existingHome.mkdir( backupDirName ); - CalamaresUtils::System::instance()-> - targetEnvCall( { "sh", - "-c", - "mv -f " + - shellFriendlyHome + "/.* " + - shellFriendlyHome + "/" + - backupDirName - } ); + CalamaresUtils::System::instance()->targetEnvCall( + { "sh", "-c", "mv -f " + shellFriendlyHome + "/.* " + shellFriendlyHome + "/" + backupDirName } ); } } - QStringList useradd{ "useradd", "-m", "-U" }; + QStringList useradd { "useradd", "-m", "-U" }; QString shell = gs->value( "userShell" ).toString(); if ( !shell.isEmpty() ) + { useradd << "-s" << shell; + } useradd << "-c" << m_fullName; useradd << m_userName; @@ -161,8 +157,8 @@ CreateUserJob::exec() return commandResult.explainProcess( useradd, std::chrono::seconds( 10 ) /* bogus timeout */ ); } - commandResult = CalamaresUtils::System::instance()->targetEnvCommand( - { "usermod", "-aG", defaultGroups, m_userName } ); + commandResult + = CalamaresUtils::System::instance()->targetEnvCommand( { "usermod", "-aG", defaultGroups, m_userName } ); if ( commandResult.getExitCode() ) { cError() << "usermod failed" << commandResult.getExitCode(); @@ -171,8 +167,7 @@ CreateUserJob::exec() QString userGroup = QString( "%1:%2" ).arg( m_userName ).arg( m_userName ); QString homeDir = QString( "/home/%1" ).arg( m_userName ); - commandResult = CalamaresUtils::System::instance()->targetEnvCommand( - { "chown", "-R", userGroup, homeDir } ); + commandResult = CalamaresUtils::System::instance()->targetEnvCommand( { "chown", "-R", userGroup, homeDir } ); if ( commandResult.getExitCode() ) { cError() << "chown failed" << commandResult.getExitCode(); diff --git a/src/modules/users/CreateUserJob.h b/src/modules/users/CreateUserJob.h index d3459fc8a..98d7c001e 100644 --- a/src/modules/users/CreateUserJob.h +++ b/src/modules/users/CreateUserJob.h @@ -27,10 +27,7 @@ class CreateUserJob : public Calamares::Job { Q_OBJECT public: - CreateUserJob( const QString& userName, - const QString& fullName, - bool autologin, - const QStringList& defaultGroups ); + CreateUserJob( const QString& userName, const QString& fullName, bool autologin, const QStringList& defaultGroups ); QString prettyName() const override; QString prettyDescription() const override; QString prettyStatusMessage() const override; diff --git a/src/modules/users/PasswordTests.cpp b/src/modules/users/PasswordTests.cpp index d4526351a..0c1b4bffc 100644 --- a/src/modules/users/PasswordTests.cpp +++ b/src/modules/users/PasswordTests.cpp @@ -24,13 +24,9 @@ QTEST_GUILESS_MAIN( PasswordTests ) -PasswordTests::PasswordTests() -{ -} +PasswordTests::PasswordTests() {} -PasswordTests::~PasswordTests() -{ -} +PasswordTests::~PasswordTests() {} void PasswordTests::initTestCase() @@ -41,7 +37,7 @@ void PasswordTests::testSalt() { QString s = SetPasswordJob::make_salt( 8 ); - QCOMPARE( s.length(), 4 + 8 ); // 8 salt chars, plus $6$, plus trailing $ + QCOMPARE( s.length(), 4 + 8 ); // 8 salt chars, plus $6$, plus trailing $ QVERIFY( s.startsWith( "$6$" ) ); QVERIFY( s.endsWith( '$' ) ); qDebug() << "Obtained salt" << s; diff --git a/src/modules/users/SetHostNameJob.cpp b/src/modules/users/SetHostNameJob.cpp index 62b1c61a7..b03d7a200 100644 --- a/src/modules/users/SetHostNameJob.cpp +++ b/src/modules/users/SetHostNameJob.cpp @@ -21,11 +21,11 @@ #include "SetHostNameJob.h" #include "GlobalStorage.h" -#include "utils/Logger.h" #include "JobQueue.h" +#include "utils/Logger.h" -#include #include +#include SetHostNameJob::SetHostNameJob( const QString& hostname ) : Calamares::Job() @@ -33,7 +33,8 @@ SetHostNameJob::SetHostNameJob( const QString& hostname ) { } -QString SetHostNameJob::prettyName() const +QString +SetHostNameJob::prettyName() const { return tr( "Set hostname %1" ).arg( m_hostname ); } @@ -52,7 +53,8 @@ SetHostNameJob::prettyStatusMessage() const return tr( "Setting hostname %1." ).arg( m_hostname ); } -Calamares::JobResult SetHostNameJob::exec() +Calamares::JobResult +SetHostNameJob::exec() { Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); @@ -90,11 +92,21 @@ Calamares::JobResult SetHostNameJob::exec() // We also need to write the appropriate entries for /etc/hosts QTextStream hostsfileout( &hostsfile ); // ipv4 support - hostsfileout << "127.0.0.1" << "\t" << "localhost" << "\n"; - hostsfileout << "127.0.1.1" << "\t" << m_hostname << "\n"; + hostsfileout << "127.0.0.1" + << "\t" + << "localhost" + << "\n"; + hostsfileout << "127.0.1.1" + << "\t" << m_hostname << "\n"; // ipv6 support - hostsfileout << "::1" << "\t" << "localhost ip6-localhost ip6-loopback" << "\n"; - hostsfileout << "ff02::1 ip6-allnodes" << "\n" << "ff02::2 ip6-allrouters" << "\n"; + hostsfileout << "::1" + << "\t" + << "localhost ip6-localhost ip6-loopback" + << "\n"; + hostsfileout << "ff02::1 ip6-allnodes" + << "\n" + << "ff02::2 ip6-allrouters" + << "\n"; hostsfile.close(); return Calamares::JobResult::ok(); diff --git a/src/modules/users/SetHostNameJob.h b/src/modules/users/SetHostNameJob.h index 11e296fce..ca2481dd4 100644 --- a/src/modules/users/SetHostNameJob.h +++ b/src/modules/users/SetHostNameJob.h @@ -31,9 +31,10 @@ public: QString prettyDescription() const override; QString prettyStatusMessage() const override; Calamares::JobResult exec() override; + private: const QString m_hostname; }; -#endif // SETHOSTNAMEJOB_CPP_H +#endif // SETHOSTNAMEJOB_CPP_H diff --git a/src/modules/users/SetPasswordJob.cpp b/src/modules/users/SetPasswordJob.cpp index 9c560106d..325d24fe3 100644 --- a/src/modules/users/SetPasswordJob.cpp +++ b/src/modules/users/SetPasswordJob.cpp @@ -19,10 +19,10 @@ #include -#include "JobQueue.h" #include "GlobalStorage.h" -#include "utils/Logger.h" +#include "JobQueue.h" #include "utils/CalamaresUtilsSystem.h" +#include "utils/Logger.h" #include @@ -58,29 +58,27 @@ SetPasswordJob::prettyStatusMessage() const /// Returns a modular hashing salt for method 6 (SHA512) with a 16 character random salt. QString -SetPasswordJob::make_salt(int length) +SetPasswordJob::make_salt( int length ) { - Q_ASSERT(length >= 8); - Q_ASSERT(length <= 128); + Q_ASSERT( length >= 8 ); + Q_ASSERT( length <= 128 ); - static const char salt_chars[] = { - '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', - 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', - 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', - 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; + static const char salt_chars[] = { '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; - static_assert( sizeof(salt_chars) == 64, "Missing salt_chars"); + static_assert( sizeof( salt_chars ) == 64, "Missing salt_chars" ); std::random_device r; - std::seed_seq seed{r(), r(), r(), r(), r(), r(), r(), r()}; - std::mt19937_64 twister(seed); + std::seed_seq seed { r(), r(), r(), r(), r(), r(), r(), r() }; + std::mt19937_64 twister( seed ); std::uint64_t next; int current_length = 0; QString salt_string; - salt_string.reserve(length + 10); + salt_string.reserve( length + 10 ); while ( current_length < length ) { @@ -89,11 +87,13 @@ SetPasswordJob::make_salt(int length) // to a single salt character. for ( unsigned int char_count = 0; char_count < 10; ++char_count ) { - char c = salt_chars[next & 0b0111111]; + char c = salt_chars[ next & 0b0111111 ]; next >>= 6; salt_string.append( c ); - if (++current_length >= length) + if ( ++current_length >= length ) + { break; + } } } @@ -112,34 +112,21 @@ SetPasswordJob::exec() return Calamares::JobResult::error( tr( "Bad destination system path." ), tr( "rootMountPoint is %1" ).arg( destDir.absolutePath() ) ); - if ( m_userName == "root" && - m_newPassword.isEmpty() ) //special case for disabling root account + if ( m_userName == "root" && m_newPassword.isEmpty() ) //special case for disabling root account { - int ec = CalamaresUtils::System::instance()-> - targetEnvCall( { "passwd", - "-dl", - m_userName } ); + int ec = CalamaresUtils::System::instance()->targetEnvCall( { "passwd", "-dl", m_userName } ); if ( ec ) return Calamares::JobResult::error( tr( "Cannot disable root account." ), - tr( "passwd terminated with error code %1." ) - .arg( ec ) ); + tr( "passwd terminated with error code %1." ).arg( ec ) ); return Calamares::JobResult::ok(); } - QString encrypted = QString::fromLatin1( - crypt( m_newPassword.toUtf8(), - make_salt( 16 ).toUtf8() ) ); + QString encrypted = QString::fromLatin1( crypt( m_newPassword.toUtf8(), make_salt( 16 ).toUtf8() ) ); - int ec = CalamaresUtils::System::instance()-> - targetEnvCall( { "usermod", - "-p", - encrypted, - m_userName } ); + int ec = CalamaresUtils::System::instance()->targetEnvCall( { "usermod", "-p", encrypted, m_userName } ); if ( ec ) - return Calamares::JobResult::error( tr( "Cannot set password for user %1." ) - .arg( m_userName ), - tr( "usermod terminated with error code %1." ) - .arg( ec ) ); + return Calamares::JobResult::error( tr( "Cannot set password for user %1." ).arg( m_userName ), + tr( "usermod terminated with error code %1." ).arg( ec ) ); return Calamares::JobResult::ok(); } diff --git a/src/modules/users/SetPasswordJob.h b/src/modules/users/SetPasswordJob.h index c4ec59c2a..d2ebf64aa 100644 --- a/src/modules/users/SetPasswordJob.h +++ b/src/modules/users/SetPasswordJob.h @@ -27,13 +27,12 @@ class SetPasswordJob : public Calamares::Job { Q_OBJECT public: - SetPasswordJob( const QString& userName, - const QString& newPassword ); + SetPasswordJob( const QString& userName, const QString& newPassword ); QString prettyName() const override; QString prettyStatusMessage() const override; Calamares::JobResult exec() override; - static QString make_salt(int length); + static QString make_salt( int length ); private: QString m_userName; diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 62292b76c..c0965a7ed 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -27,8 +27,8 @@ #include "ui_page_usersetup.h" #include "CreateUserJob.h" -#include "SetPasswordJob.h" #include "SetHostNameJob.h" +#include "SetPasswordJob.h" #include "GlobalStorage.h" #include "JobQueue.h" @@ -45,12 +45,28 @@ #include #include +static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); +static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); +static constexpr const int USERNAME_MAX_LENGTH = 31; +static constexpr const int HOSTNAME_MIN_LENGTH = 2; +static constexpr const int HOSTNAME_MAX_LENGTH = 63; + +/** @brief How bad is the error for labelError() ? */ +enum class Badness +{ + Fatal, + Warning +}; + /** Add an error message and pixmap to a label. */ static inline void -labelError( QLabel* pix, QLabel* label, const QString& message ) +labelError( QLabel* pix, QLabel* label, const QString& message, Badness bad = Badness::Fatal ) { label->setText( message ); - pix->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::No, CalamaresUtils::Original, label->size() ) ); + pix->setPixmap( CalamaresUtils::defaultPixmap( ( bad == Badness::Fatal ) ? CalamaresUtils::StatusError + : CalamaresUtils::StatusWarning, + CalamaresUtils::Original, + label->size() ) ); } /** Clear error, indicate OK on a label. */ @@ -74,25 +90,20 @@ UsersPage::UsersPage( QWidget* parent ) ui->setupUi( this ); // Connect signals and slots - connect( ui->textBoxFullName, &QLineEdit::textEdited, - this, &UsersPage::onFullNameTextEdited ); - connect( ui->textBoxUsername, &QLineEdit::textEdited, - this, &UsersPage::onUsernameTextEdited ); - connect( ui->textBoxHostname, &QLineEdit::textEdited, - this, &UsersPage::onHostnameTextEdited ); - connect( ui->textBoxUserPassword, &QLineEdit::textChanged, - this, &UsersPage::onPasswordTextChanged ); - connect( ui->textBoxUserVerifiedPassword, &QLineEdit::textChanged, - this, &UsersPage::onPasswordTextChanged ); - connect( ui->textBoxRootPassword, &QLineEdit::textChanged, - this, &UsersPage::onRootPasswordTextChanged ); - connect( ui->textBoxVerifiedRootPassword, &QLineEdit::textChanged, - this, &UsersPage::onRootPasswordTextChanged ); - connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, - this, [this]( int checked ) - { + connect( ui->textBoxFullName, &QLineEdit::textEdited, this, &UsersPage::onFullNameTextEdited ); + connect( ui->textBoxUsername, &QLineEdit::textEdited, this, &UsersPage::onUsernameTextEdited ); + connect( ui->textBoxHostname, &QLineEdit::textEdited, this, &UsersPage::onHostnameTextEdited ); + connect( ui->textBoxUserPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); + connect( ui->textBoxUserVerifiedPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); + connect( ui->textBoxRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged ); + connect( ui->textBoxVerifiedRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged ); + connect( ui->checkBoxValidatePassword, &QCheckBox::stateChanged, this, [this]( int checked ) { + onPasswordTextChanged( ui->textBoxUserPassword->text() ); + onRootPasswordTextChanged( ui->textBoxRootPassword->text() ); + checkReady( isReady() ); + } ); + connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [this]( int checked ) { ui->labelChooseRootPassword->setVisible( !checked ); - ui->labelExtraRootPassword->setVisible( !checked ); ui->labelRootPassword->setVisible( !checked ); ui->labelRootPasswordError->setVisible( !checked ); ui->textBoxRootPassword->setVisible( !checked ); @@ -105,47 +116,45 @@ UsersPage::UsersPage( QWidget* parent ) setWriteRootPassword( true ); ui->checkBoxReusePassword->setChecked( true ); + ui->checkBoxValidatePassword->setChecked( true ); - // Don't expand the explanations to "stupid wide", but keep them vaguely as-wide-as - // the things they are explaining. - int boxWidth = qMax( qMax( ui->textBoxUsername->width(), ui->textBoxHostname->width() ), ui->textBoxUserPassword->width() ); - ui->username_extra_label_2->setMaximumWidth( 3 * boxWidth ); - ui->hostname_extra_label_2->setMaximumWidth( 3 * boxWidth ); - ui->password_extra_label_3->setMaximumWidth( 3 * boxWidth ); + setPasswordCheckboxVisible( false ); - CALAMARES_RETRANSLATE( - ui->retranslateUi( this ); - if ( Calamares::Settings::instance()->isSetupMode() ) - { - ui->username_extra_label_2->setText( tr( "If more than one person will " - "use this computer, you can create multiple " - "accounts after setup." ) ); - } - else - { - ui->username_extra_label_2->setText( tr( "If more than one person will " - "use this computer, you can create multiple " - "accounts after installation." ) ); - } - ) + CALAMARES_RETRANSLATE_SLOT( &UsersPage::retranslate ); } - UsersPage::~UsersPage() { delete ui; } +void +UsersPage::retranslate() +{ + ui->retranslateUi( this ); + if ( Calamares::Settings::instance()->isSetupMode() ) + { + ui->textBoxUsername->setToolTip( tr( "If more than one person will " + "use this computer, you can create multiple " + "accounts after setup." ) ); + } + else + { + ui->textBoxUsername->setToolTip( tr( "If more than one person will " + "use this computer, you can create multiple " + "accounts after installation." ) ); + } +} + bool UsersPage::isReady() { - bool readyFields = m_readyFullName && - m_readyHostname && - m_readyPassword && - m_readyUsername; + bool readyFields = m_readyFullName && m_readyHostname && m_readyPassword && m_readyUsername; if ( !m_writeRootPassword || ui->checkBoxReusePassword->isChecked() ) + { return readyFields; + } return readyFields && m_readyRootPassword; } @@ -156,38 +165,40 @@ UsersPage::createJobs( const QStringList& defaultGroupsList ) { QList< Calamares::job_ptr > list; if ( !isReady() ) + { return list; + } Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); Calamares::Job* j; j = new CreateUserJob( ui->textBoxUsername->text(), - ui->textBoxFullName->text().isEmpty() ? - ui->textBoxUsername->text() : - ui->textBoxFullName->text(), + ui->textBoxFullName->text().isEmpty() ? ui->textBoxUsername->text() + : ui->textBoxFullName->text(), ui->checkBoxAutoLogin->isChecked(), defaultGroupsList ); list.append( Calamares::job_ptr( j ) ); - j = new SetPasswordJob( ui->textBoxUsername->text(), - ui->textBoxUserPassword->text() ); + j = new SetPasswordJob( ui->textBoxUsername->text(), ui->textBoxUserPassword->text() ); list.append( Calamares::job_ptr( j ) ); if ( m_writeRootPassword ) { gs->insert( "reuseRootPassword", ui->checkBoxReusePassword->isChecked() ); if ( ui->checkBoxReusePassword->isChecked() ) - j = new SetPasswordJob( "root", - ui->textBoxUserPassword->text() ); + { + j = new SetPasswordJob( "root", ui->textBoxUserPassword->text() ); + } else - j = new SetPasswordJob( "root", - ui->textBoxRootPassword->text() ); + { + j = new SetPasswordJob( "root", ui->textBoxRootPassword->text() ); + } list.append( Calamares::job_ptr( j ) ); } else { j = new SetPasswordJob( "root", - "" ); //explicitly disable root password + "" ); //explicitly disable root password list.append( Calamares::job_ptr( j ) ); } @@ -196,7 +207,9 @@ UsersPage::createJobs( const QStringList& defaultGroupsList ) gs->insert( "hostname", ui->textBoxHostname->text() ); if ( ui->checkBoxAutoLogin->isChecked() ) + { gs->insert( "autologinUser", ui->textBoxUsername->text() ); + } gs->insert( "username", ui->textBoxUsername->text() ); gs->insert( "password", CalamaresUtils::obscure( ui->textBoxUserPassword->text() ) ); @@ -228,16 +241,19 @@ UsersPage::onFullNameTextEdited( const QString& textRef ) ui->labelFullNameError->clear(); ui->labelFullName->clear(); if ( !m_customUsername ) + { ui->textBoxUsername->clear(); + } if ( !m_customHostname ) + { ui->textBoxHostname->clear(); + } m_readyFullName = false; } else { - ui->labelFullName->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, - CalamaresUtils::Original, - ui->labelFullName->size() ) ); + ui->labelFullName->setPixmap( + CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, ui->labelFullName->size() ) ); m_readyFullName = true; fillSuggestions(); } @@ -250,8 +266,7 @@ UsersPage::fillSuggestions() { QString fullName = ui->textBoxFullName->text(); QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive ); - QString cleanName = CalamaresUtils::removeDiacritics( fullName ) - .toLower().replace( rx, " " ).simplified(); + QString cleanName = CalamaresUtils::removeDiacritics( fullName ).toLower().replace( rx, " " ).simplified(); QStringList cleanParts = cleanName.split( ' ' ); if ( !m_customUsername ) @@ -262,7 +277,9 @@ UsersPage::fillSuggestions() for ( int i = 1; i < cleanParts.length(); ++i ) { if ( !cleanParts.value( i ).isEmpty() ) + { usernameSuggestion.append( cleanParts.value( i ).at( 0 ) ); + } } if ( USERNAME_RX.indexIn( usernameSuggestion ) != -1 ) { @@ -301,8 +318,8 @@ void UsersPage::validateUsernameText( const QString& textRef ) { QString text( textRef ); - QRegExp rx( USERNAME_RX ); - QRegExpValidator val( rx ); + QRegExpValidator val_whole( USERNAME_RX ); + QRegExpValidator val_start( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator int pos = -1; if ( text.isEmpty() ) @@ -313,14 +330,21 @@ UsersPage::validateUsernameText( const QString& textRef ) } else if ( text.length() > USERNAME_MAX_LENGTH ) { - labelError( ui->labelUsername, ui->labelUsernameError, - tr( "Your username is too long." ) ); + labelError( ui->labelUsername, ui->labelUsernameError, tr( "Your username is too long." ) ); m_readyUsername = false; } - else if ( val.validate( text, pos ) == QValidator::Invalid ) + else if ( val_start.validate( text, pos ) == QValidator::Invalid ) { - labelError( ui->labelUsername, ui->labelUsernameError, - tr( "Your username contains invalid characters. Only lowercase letters and numbers are allowed." ) ); + labelError( ui->labelUsername, + ui->labelUsernameError, + tr( "Your username must start with a lowercase letter or underscore." ) ); + m_readyUsername = false; + } + else if ( val_whole.validate( text, pos ) == QValidator::Invalid ) + { + labelError( ui->labelUsername, + ui->labelUsernameError, + tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ) ); m_readyUsername = false; } else @@ -345,32 +369,30 @@ void UsersPage::validateHostnameText( const QString& textRef ) { QString text = textRef; - QRegExp rx( HOSTNAME_RX ); - QRegExpValidator val( rx ); + QRegExpValidator val( HOSTNAME_RX ); int pos = -1; if ( text.isEmpty() ) { ui->labelHostnameError->clear(); ui->labelHostname->clear(); - m_readyHostname= false; + m_readyHostname = false; } else if ( text.length() < HOSTNAME_MIN_LENGTH ) { - labelError( ui->labelHostname, ui->labelHostnameError, - tr( "Your hostname is too short." ) ); + labelError( ui->labelHostname, ui->labelHostnameError, tr( "Your hostname is too short." ) ); m_readyHostname = false; } else if ( text.length() > HOSTNAME_MAX_LENGTH ) { - labelError( ui->labelHostname, ui->labelHostnameError, - tr( "Your hostname is too long." ) ); + labelError( ui->labelHostname, ui->labelHostnameError, tr( "Your hostname is too long." ) ); m_readyHostname = false; } else if ( val.validate( text, pos ) == QValidator::Invalid ) { - labelError( ui->labelHostname, ui->labelHostnameError, - tr( "Your hostname contains invalid characters. Only letters, numbers and dashes are allowed." ) ); + labelError( ui->labelHostname, + ui->labelHostnameError, + tr( "Only letters, numbers, underscore and hyphen are allowed." ) ); m_readyHostname = false; } else @@ -382,46 +404,59 @@ UsersPage::validateHostnameText( const QString& textRef ) emit checkReady( isReady() ); } -void -UsersPage::onPasswordTextChanged( const QString& ) +bool +UsersPage::checkPasswordAcceptance( const QString& pw1, const QString& pw2, QLabel* badge, QLabel* message ) { - QString pw1 = ui->textBoxUserPassword->text(); - QString pw2 = ui->textBoxUserVerifiedPassword->text(); - - // TODO: 3.3: remove empty-check and leave it to passwordRequirements if ( pw1.isEmpty() && pw2.isEmpty() ) { - ui->labelUserPasswordError->clear(); - ui->labelUserPassword->clear(); - m_readyPassword = false; + // Not exactly labelOk() because we also don't want a checkmark OK + badge->clear(); + message->clear(); + return false; } else if ( pw1 != pw2 ) { - labelError( ui->labelUserPassword, ui->labelUserPasswordError, - tr( "Your passwords do not match!" ) ); - m_readyPassword = false; + labelError( badge, message, tr( "Your passwords do not match!" ) ); + return false; } else { - bool ok = true; + bool failureIsFatal = ui->checkBoxValidatePassword->isChecked(); + bool failureFound = false; + for ( auto pc : m_passwordChecks ) { QString s = pc.filter( pw1 ); + if ( !s.isEmpty() ) { - labelError( ui->labelUserPassword, ui->labelUserPasswordError, s ); - ok = false; - m_readyPassword = false; - break; + labelError( badge, message, s, failureIsFatal ? Badness::Fatal : Badness::Warning ); + failureFound = true; + if ( failureIsFatal ) + { + return false; + } } } - if ( ok ) + if ( !failureFound ) { - labelOk( ui->labelUserPassword, ui->labelUserPasswordError ); - m_readyPassword = true; + labelOk( badge, message ); } + + // Here, if failureFound is true then we've found **warnings**, + // which is ok to continue but the user should know. + return true; } +} + +void +UsersPage::onPasswordTextChanged( const QString& ) +{ + m_readyPassword = checkPasswordAcceptance( ui->textBoxUserPassword->text(), + ui->textBoxUserVerifiedPassword->text(), + ui->labelUserPassword, + ui->labelUserPasswordError ); emit checkReady( isReady() ); } @@ -429,48 +464,27 @@ UsersPage::onPasswordTextChanged( const QString& ) void UsersPage::onRootPasswordTextChanged( const QString& ) { - QString pw1 = ui->textBoxRootPassword->text(); - QString pw2 = ui->textBoxVerifiedRootPassword->text(); - - // TODO: 3.3: remove empty-check and leave it to passwordRequirements - if ( pw1.isEmpty() && pw2.isEmpty() ) - { - ui->labelRootPasswordError->clear(); - ui->labelRootPassword->clear(); - m_readyRootPassword = false; - } - else if ( pw1 != pw2 ) - { - labelError( ui->labelRootPassword, ui->labelRootPasswordError, - tr( "Your passwords do not match!" ) ); - m_readyRootPassword = false; - } - else - { - bool ok = true; - for ( auto pc : m_passwordChecks ) - { - QString s = pc.filter( pw1 ); - if ( !s.isEmpty() ) - { - labelError( ui->labelRootPassword, ui->labelRootPasswordError, s ); - ok = false; - m_readyRootPassword = false; - break; - } - } - - if ( ok ) - { - labelOk( ui->labelRootPassword, ui->labelRootPasswordError ); - m_readyRootPassword = true; - } - } - + m_readyRootPassword = checkPasswordAcceptance( ui->textBoxRootPassword->text(), + ui->textBoxVerifiedRootPassword->text(), + ui->labelRootPassword, + ui->labelRootPasswordError ); emit checkReady( isReady() ); } +void +UsersPage::setPasswordCheckboxVisible( bool visible ) +{ + ui->checkBoxValidatePassword->setVisible( visible ); +} + +void +UsersPage::setValidatePasswordDefault( bool checked ) +{ + ui->checkBoxValidatePassword->setChecked( checked ); + emit checkReady( isReady() ); +} + void UsersPage::setAutologinDefault( bool checked ) { @@ -501,7 +515,9 @@ UsersPage::addPasswordCheck( const QString& key, const QVariant& value ) { add_check_libpwquality( m_passwordChecks, value ); } -#endif +#endif // CHECK_PWQUALITY else + { cWarning() << "Unknown password-check key" << key; + } } diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index ac1e1f9db..a2befbd26 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -29,6 +29,8 @@ #include +class QLabel; + namespace Ui { class Page_UserSetup; @@ -48,6 +50,8 @@ public: void onActivate(); void setWriteRootPassword( bool show ); + void setPasswordCheckboxVisible( bool visible ); + void setValidatePasswordDefault( bool checked ); void setAutologinDefault( bool checked ); void setReusePasswordDefault( bool checked ); @@ -73,16 +77,20 @@ signals: void checkReady( bool ); private: + /** @brief Is the password acceptable? + * + * Checks the two copies of the password and places error messages in the + * given QLabels. Returns true (and clears the error messages) if the + * password is acceptable. + */ + bool checkPasswordAcceptance( const QString& pw1, const QString& pw2, QLabel* badge, QLabel* message ); + + void retranslate(); + Ui::Page_UserSetup* ui; PasswordCheckList 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; - const int HOSTNAME_MIN_LENGTH = 2; - const int HOSTNAME_MAX_LENGTH = 63; - bool m_readyFullName; bool m_readyUsername; bool m_customUsername; @@ -94,4 +102,4 @@ private: bool m_writeRootPassword; }; -#endif // USERSPAGE_H +#endif // USERSPAGE_H diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index 0d1bb593e..b898f00c8 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -29,22 +29,23 @@ #include "GlobalStorage.h" #include "JobQueue.h" -CALAMARES_PLUGIN_FACTORY_DEFINITION( UsersViewStepFactory, registerPlugin(); ) +CALAMARES_PLUGIN_FACTORY_DEFINITION( UsersViewStepFactory, registerPlugin< UsersViewStep >(); ) UsersViewStep::UsersViewStep( QObject* parent ) : Calamares::ViewStep( parent ) , m_widget( new UsersPage() ) { emit nextStatusChanged( true ); - connect( m_widget, &UsersPage::checkReady, - this, &UsersViewStep::nextStatusChanged ); + connect( m_widget, &UsersPage::checkReady, this, &UsersViewStep::nextStatusChanged ); } UsersViewStep::~UsersViewStep() { if ( m_widget && m_widget->parent() == nullptr ) + { m_widget->deleteLater(); + } } @@ -116,68 +117,71 @@ UsersViewStep::onLeave() void UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { - if ( configurationMap.contains( "defaultGroups" ) && - configurationMap.value( "defaultGroups" ).type() == QVariant::List ) + if ( configurationMap.contains( "defaultGroups" ) + && configurationMap.value( "defaultGroups" ).type() == QVariant::List ) { m_defaultGroups = configurationMap.value( "defaultGroups" ).toStringList(); } else { cWarning() << "Using fallback groups. Please check defaultGroups in users.conf"; - m_defaultGroups = QStringList{ "lp", "video", "network", "storage", "wheel", "audio" }; + m_defaultGroups = QStringList { "lp", "video", "network", "storage", "wheel", "audio" }; } - if ( configurationMap.contains( "autologinGroup" ) && - configurationMap.value( "autologinGroup" ).type() == QVariant::String ) + if ( configurationMap.contains( "autologinGroup" ) + && configurationMap.value( "autologinGroup" ).type() == QVariant::String ) { - Calamares::JobQueue::instance()->globalStorage()->insert( "autologinGroup", - configurationMap.value( "autologinGroup" ).toString() ); + Calamares::JobQueue::instance()->globalStorage()->insert( + "autologinGroup", configurationMap.value( "autologinGroup" ).toString() ); } - if ( configurationMap.contains( "sudoersGroup" ) && - configurationMap.value( "sudoersGroup" ).type() == QVariant::String ) + if ( configurationMap.contains( "sudoersGroup" ) + && configurationMap.value( "sudoersGroup" ).type() == QVariant::String ) { Calamares::JobQueue::instance()->globalStorage()->insert( "sudoersGroup", - configurationMap.value( "sudoersGroup" ).toString() ); + configurationMap.value( "sudoersGroup" ).toString() ); } - if ( configurationMap.contains( "setRootPassword" ) && - configurationMap.value( "setRootPassword" ).type() == QVariant::Bool ) + if ( configurationMap.contains( "setRootPassword" ) + && configurationMap.value( "setRootPassword" ).type() == QVariant::Bool ) { - Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", - configurationMap.value( "setRootPassword" ).toBool() ); + Calamares::JobQueue::instance()->globalStorage()->insert( + "setRootPassword", configurationMap.value( "setRootPassword" ).toBool() ); m_widget->setWriteRootPassword( configurationMap.value( "setRootPassword" ).toBool() ); } - if ( configurationMap.contains( "doAutologin" ) && - configurationMap.value( "doAutologin" ).type() == QVariant::Bool ) + if ( configurationMap.contains( "doAutologin" ) + && configurationMap.value( "doAutologin" ).type() == QVariant::Bool ) { m_widget->setAutologinDefault( configurationMap.value( "doAutologin" ).toBool() ); } - if ( configurationMap.contains( "doReusePassword" ) && - configurationMap.value( "doReusePassword" ).type() == QVariant::Bool ) + 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 ) + 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) + for ( decltype( pr_checks )::const_iterator i = pr_checks.constBegin(); i != pr_checks.constEnd(); ++i ) { m_widget->addPasswordCheck( i.key(), i.value() ); } } + m_widget->setPasswordCheckboxVisible( CalamaresUtils::getBool( configurationMap, "allowWeakPasswords", false ) ); + m_widget->setValidatePasswordDefault( !CalamaresUtils::getBool( configurationMap, "allowWeakPasswordsDefault", false) ); + QString shell( QLatin1String( "/bin/bash" ) ); // as if it's not set at all if ( configurationMap.contains( "userShell" ) ) + { shell = CalamaresUtils::getString( configurationMap, "userShell" ); - // Now it might be explicitly set to empty, which is ok + } + // Now it might be explicitly set to empty, which is ok Calamares::JobQueue::instance()->globalStorage()->insert( "userShell", shell ); } - diff --git a/src/modules/users/UsersViewStep.h b/src/modules/users/UsersViewStep.h index a1995497c..6fced76b9 100644 --- a/src/modules/users/UsersViewStep.h +++ b/src/modules/users/UsersViewStep.h @@ -65,4 +65,4 @@ private: CALAMARES_PLUGIN_FACTORY_DECLARATION( UsersViewStepFactory ) -#endif // USERSPAGEPLUGIN_H +#endif // USERSPAGEPLUGIN_H diff --git a/src/modules/users/page_usersetup.ui b/src/modules/users/page_usersetup.ui index c93912c01..b778647d8 100644 --- a/src/modules/users/page_usersetup.ui +++ b/src/modules/users/page_usersetup.ui @@ -47,6 +47,9 @@ 0 + + Your Full Name + @@ -137,6 +140,9 @@ 0 + + login + @@ -191,19 +197,6 @@ - - - - font-weight: normal - - - <Username extra label 2 text> - - - true - - - @@ -246,6 +239,12 @@ 0 + + <small>This name will be used if you make the computer visible to others on a network.</small> + + + Computer Name + @@ -300,19 +299,6 @@ - - - - font-weight: normal - - - <small>This name will be used if you make the computer visible to others on a network.</small> - - - false - - - @@ -355,9 +341,15 @@ 0 + + <small>Enter the same password twice, so that it can be checked for typing errors. A good password will contain a mixture of letters, numbers and punctuation, should be at least eight characters long, and should be changed at regular intervals.</small> + QLineEdit::Password + + Password + @@ -374,9 +366,15 @@ 0 + + <small>Enter the same password twice, so that it can be checked for typing errors. A good password will contain a mixture of letters, numbers and punctuation, should be at least eight characters long, and should be changed at regular intervals.</small> + QLineEdit::Password + + Repeat Password + @@ -431,19 +429,6 @@ - - - - font-weight: normal - - - <small>Enter the same password twice, so that it can be checked for typing errors. A good password will contain a mixture of letters, numbers and punctuation, should be at least eight characters long, and should be changed at regular intervals.</small> - - - true - - - @@ -460,6 +445,16 @@ + + + + When this box is checked, password-strength checking is done and you will not be able to use a weak password. + + + Require strong passwords. + + + @@ -516,9 +511,15 @@ 0 + + <small>Enter the same password twice, so that it can be checked for typing errors.</small> + QLineEdit::Password + + Password + @@ -535,9 +536,15 @@ 0 + + <small>Enter the same password twice, so that it can be checked for typing errors.</small> + QLineEdit::Password + + Repeat Password + @@ -592,19 +599,6 @@ - - - - font-weight: normal - - - <small>Enter the same password twice, so that it can be checked for typing errors.</small> - - - true - - - diff --git a/src/modules/users/users.conf b/src/modules/users/users.conf index 0c40faeff..cae9bef0d 100644 --- a/src/modules/users/users.conf +++ b/src/modules/users/users.conf @@ -27,26 +27,34 @@ defaultGroups: # the desktop environment on boot. # Disable when your Distribution does not require such a group. autologinGroup: autologin -# You can control the initial state for the 'autologin checkbox' in UsersViewStep here. -# Possible values are: true to enable or false to disable the checkbox by default +# You can control the initial state for the 'autologin checkbox' here. +# Possible values are: +# - true to check or +# - false to uncheck +# These set the **initial** state of the checkbox. doAutologin: true -# When set to a non-empty string, Calamares creates a sudoers file for the user. -# /etc/sudoers.d/10-installer -# Remember to add sudoersGroup to defaultGroups. +# When *sudoersGroup* is set to a non-empty string, Calamares creates a +# sudoers file for the user. This file is located at: +# `/etc/sudoers.d/10-installer` +# Remember to add the (value of) *sudoersGroup* to *defaultGroups*. # # If your Distribution already sets up a group of sudoers in its packaging, # remove this setting (delete or comment out the line below). Otherwise, -# the setting will be duplicated in the /etc/sudoers.d/10-installer file, +# the setting will be duplicated in the `/etc/sudoers.d/10-installer` file, # potentially confusing users. sudoersGroup: wheel # Setting this to false , causes the root account to be disabled. setRootPassword: true -# You can control the initial state for the 'root password checkbox' in UsersViewStep here. -# Possible values are: true to enable or false to disable the checkbox by default. -# When enabled the user password is used for the root account too. -# NOTE: doReusePassword requires setRootPassword to be enabled. +# You can control the initial state for the 'reuse password for root' +# checkbox here. Possible values are: +# - true to check or +# - false to uncheck +# +# When checked, the user password is used for the root account too. +# +# NOTE: *doReusePassword* requires *setRootPassword* to be enabled. doReusePassword: true # These are optional password-requirements that a distro can enforce @@ -68,12 +76,36 @@ doReusePassword: true # # (additional checks may be implemented in CheckPWQuality.cpp and # wired into UsersPage.cpp) +# +# - To disable specific password validations: +# comment out the relevant 'passwordRequirements' keys below. +# - To disable all password validations: +# set both 'allowWeakPasswords' and 'allowWeakPasswordsDefault' to true. +# (That will show the box *Allow weak passwords* in the user- +# interface, and check it by default). passwordRequirements: minLength: -1 # Password at least this many characters maxLength: -1 # Password at most this many characters libpwquality: - minlen=0 - minclass=0 + +# You can control the visibility of the 'strong passwords' checkbox here. +# Possible values are: +# - true to show or +# - false to hide (default) +# the checkbox. This checkbox allows the user to choose to disable +# password-strength-checks. By default the box is **hidden**, so +# that you have to pick a password that satisfies the checks. +allowWeakPasswords: false +# You can control the initial state for the 'strong passwords' checkbox here. +# Possible values are: +# - true to uncheck or +# - false to check (default) +# the checkbox by default. Since the box is labeled to enforce strong +# passwords, in order to **allow** weak ones by default, the box needs +# to be unchecked. +allowWeakPasswordsDefault: false # Shell to be used for the regular user of the target system. # There are three possible kinds of settings: