1 /* === This file is part of Calamares - <https://calamares.io> ===
2  *
3  *   SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
4  *   SPDX-License-Identifier: GPL-3.0-or-later
5  *
6  *   Calamares is Free Software: see the License-Identifier above.
7  *
8  */
9 
10 #include "Config.h"
11 
12 #include "CreateUserJob.h"
13 #include "MiscJobs.h"
14 #include "SetHostNameJob.h"
15 #include "SetPasswordJob.h"
16 
17 #include "GlobalStorage.h"
18 #include "JobQueue.h"
19 #include "utils/Logger.h"
20 #include "utils/String.h"
21 #include "utils/Variant.h"
22 
23 #include <QCoreApplication>
24 #include <QFile>
25 #include <QMetaProperty>
26 #include <QRegExp>
27 #include <QTimer>
28 
29 #ifdef HAVE_ICU
30 #include <unicode/translit.h>
31 #include <unicode/unistr.h>
32 
33 //Needed for ICU to apply some transliteration ruleset.
34 //Still needs to be adjusted to fit the needs of the most of users
35 static const char TRANSLITERATOR_ID[] = "Russian-Latin/BGN;"
36                                         "Greek-Latin/UNGEGN;"
37                                         "Any-Latin;"
38                                         "Latin-ASCII";
39 #endif
40 
41 #include <memory>
42 
43 static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" );
44 static constexpr const int USERNAME_MAX_LENGTH = 31;
45 
46 static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" );
47 static constexpr const int HOSTNAME_MIN_LENGTH = 2;
48 static constexpr const int HOSTNAME_MAX_LENGTH = 63;
49 
50 static void
updateGSAutoLogin(bool doAutoLogin,const QString & login)51 updateGSAutoLogin( bool doAutoLogin, const QString& login )
52 {
53     Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
54     if ( !gs )
55     {
56         cWarning() << "No Global Storage available";
57         return;
58     }
59 
60     if ( doAutoLogin && !login.isEmpty() )
61     {
62         gs->insert( "autoLoginUser", login );
63     }
64     else
65     {
66         gs->remove( "autoLoginUser" );
67     }
68 
69     if ( login.isEmpty() )
70     {
71         gs->remove( "username" );
72     }
73     else
74     {
75         gs->insert( "username", login );
76     }
77 }
78 
79 const NamedEnumTable< HostNameAction >&
hostNameActionNames()80 hostNameActionNames()
81 {
82     // *INDENT-OFF*
83     // clang-format off
84     static const NamedEnumTable< HostNameAction > names {
85         { QStringLiteral( "none" ), HostNameAction::None },
86         { QStringLiteral( "etcfile" ), HostNameAction::EtcHostname },
87         { QStringLiteral( "hostnamed" ), HostNameAction::SystemdHostname }
88     };
89     // clang-format on
90     // *INDENT-ON*
91 
92     return names;
93 }
94 
Config(QObject * parent)95 Config::Config( QObject* parent )
96     : Calamares::ModuleSystem::Config( parent )
97 {
98     emit readyChanged( m_isReady );  // false
99 
100     // Gang together all the changes of status to one readyChanged() signal
101     connect( this, &Config::hostNameStatusChanged, this, &Config::checkReady );
102     connect( this, &Config::loginNameStatusChanged, this, &Config::checkReady );
103     connect( this, &Config::fullNameChanged, this, &Config::checkReady );
104     connect( this, &Config::userPasswordStatusChanged, this, &Config::checkReady );
105     connect( this, &Config::rootPasswordStatusChanged, this, &Config::checkReady );
106     connect( this, &Config::reuseUserPasswordForRootChanged, this, &Config::checkReady );
107     connect( this, &Config::requireStrongPasswordsChanged, this, &Config::checkReady );
108 }
109 
~Config()110 Config::~Config() {}
111 
112 void
setUserShell(const QString & shell)113 Config::setUserShell( const QString& shell )
114 {
115     if ( !shell.isEmpty() && !shell.startsWith( '/' ) )
116     {
117         cWarning() << "User shell" << shell << "is not an absolute path.";
118         return;
119     }
120     if ( shell != m_userShell )
121     {
122         m_userShell = shell;
123         emit userShellChanged( shell );
124         // The shell is put into GS as well.
125         auto* gs = Calamares::JobQueue::instance()->globalStorage();
126         if ( gs )
127         {
128             gs->insert( "userShell", shell );
129         }
130     }
131 }
132 
133 static inline void
insertInGlobalStorage(const QString & key,const QString & group)134 insertInGlobalStorage( const QString& key, const QString& group )
135 {
136     auto* gs = Calamares::JobQueue::instance()->globalStorage();
137     if ( !gs || group.isEmpty() )
138     {
139         return;
140     }
141     gs->insert( key, group );
142 }
143 
144 void
setAutoLoginGroup(const QString & group)145 Config::setAutoLoginGroup( const QString& group )
146 {
147     if ( group != m_autoLoginGroup )
148     {
149         m_autoLoginGroup = group;
150         insertInGlobalStorage( QStringLiteral( "autoLoginGroup" ), group );
151         emit autoLoginGroupChanged( group );
152     }
153 }
154 
155 QStringList
groupsForThisUser() const156 Config::groupsForThisUser() const
157 {
158     QStringList l;
159     l.reserve( defaultGroups().size() + 1 );
160 
161     for ( const auto& g : defaultGroups() )
162     {
163         l << g.name();
164     }
165     if ( doAutoLogin() && !autoLoginGroup().isEmpty() )
166     {
167         l << autoLoginGroup();
168     }
169 
170     return l;
171 }
172 
173 void
setSudoersGroup(const QString & group)174 Config::setSudoersGroup( const QString& group )
175 {
176     if ( group != m_sudoersGroup )
177     {
178         m_sudoersGroup = group;
179         insertInGlobalStorage( QStringLiteral( "sudoersGroup" ), group );
180         emit sudoersGroupChanged( group );
181     }
182 }
183 
184 
185 void
setLoginName(const QString & login)186 Config::setLoginName( const QString& login )
187 {
188     CONFIG_PREVENT_EDITING( QString, "loginName" );
189     if ( login != m_loginName )
190     {
191         m_customLoginName = !login.isEmpty();
192         m_loginName = login;
193         updateGSAutoLogin( doAutoLogin(), login );
194         emit loginNameChanged( login );
195         emit loginNameStatusChanged( loginNameStatus() );
196     }
197 }
198 
199 const QStringList&
forbiddenLoginNames()200 Config::forbiddenLoginNames()
201 {
202     static QStringList forbidden { "root" };
203     return forbidden;
204 }
205 
206 QString
loginNameStatus() const207 Config::loginNameStatus() const
208 {
209     // An empty login is "ok", even if it isn't really
210     if ( m_loginName.isEmpty() )
211     {
212         return QString();
213     }
214 
215     if ( m_loginName.length() > USERNAME_MAX_LENGTH )
216     {
217         return tr( "Your username is too long." );
218     }
219     for ( const QString& badName : forbiddenLoginNames() )
220     {
221         if ( 0 == QString::compare( badName, m_loginName, Qt::CaseSensitive ) )
222         {
223             return tr( "'%1' is not allowed as username." ).arg( badName );
224         }
225     }
226 
227     QRegExp validateFirstLetter( "^[a-z_]" );
228     if ( validateFirstLetter.indexIn( m_loginName ) != 0 )
229     {
230         return tr( "Your username must start with a lowercase letter or underscore." );
231     }
232     if ( !USERNAME_RX.exactMatch( m_loginName ) )
233     {
234         return tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." );
235     }
236 
237     return QString();
238 }
239 
240 void
setHostName(const QString & host)241 Config::setHostName( const QString& host )
242 {
243     if ( host != m_hostName )
244     {
245         m_customHostName = !host.isEmpty();
246         m_hostName = host;
247         Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
248         if ( host.isEmpty() )
249         {
250             gs->remove( "hostname" );
251         }
252         else
253         {
254             gs->insert( "hostname", host );
255         }
256         emit hostNameChanged( host );
257         emit hostNameStatusChanged( hostNameStatus() );
258     }
259 }
260 
261 const QStringList&
forbiddenHostNames()262 Config::forbiddenHostNames()
263 {
264     static QStringList forbidden { "localhost" };
265     return forbidden;
266 }
267 
268 QString
hostNameStatus() const269 Config::hostNameStatus() const
270 {
271     // An empty hostname is "ok", even if it isn't really
272     if ( m_hostName.isEmpty() )
273     {
274         return QString();
275     }
276 
277     if ( m_hostName.length() < HOSTNAME_MIN_LENGTH )
278     {
279         return tr( "Your hostname is too short." );
280     }
281     if ( m_hostName.length() > HOSTNAME_MAX_LENGTH )
282     {
283         return tr( "Your hostname is too long." );
284     }
285     for ( const QString& badName : forbiddenHostNames() )
286     {
287         if ( 0 == QString::compare( badName, m_hostName, Qt::CaseSensitive ) )
288         {
289             return tr( "'%1' is not allowed as hostname." ).arg( badName );
290         }
291     }
292 
293     if ( !HOSTNAME_RX.exactMatch( m_hostName ) )
294     {
295         return tr( "Only letters, numbers, underscore and hyphen are allowed." );
296     }
297 
298     return QString();
299 }
300 
301 
302 /** @brief Guess the machine's name
303  *
304  * If there is DMI data, use that; otherwise, just call the machine "-pc".
305  * Reads the DMI data just once.
306  */
307 static QString
guessProductName()308 guessProductName()
309 {
310     static bool tried = false;
311     static QString dmiProduct;
312 
313     if ( !tried )
314     {
315         // yes validateHostnameText() but these files can be a mess
316         QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive );
317         QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) );
318 
319         if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) )
320         {
321             dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() )
322                              .toLower()
323                              .replace( dmirx, " " )
324                              .remove( ' ' );
325         }
326         if ( dmiProduct.isEmpty() )
327         {
328             dmiProduct = QStringLiteral( "pc" );
329         }
330         tried = true;
331     }
332     return dmiProduct;
333 }
334 #ifdef HAVE_ICU
335 static QString
transliterate(const QString & input)336 transliterate( const QString& input )
337 {
338     static auto ue = UErrorCode::U_ZERO_ERROR;
339     static auto transliterator = std::unique_ptr< icu::Transliterator >(
340         icu::Transliterator::createInstance( TRANSLITERATOR_ID, UTRANS_FORWARD, ue ) );
341 
342     if ( ue != UErrorCode::U_ZERO_ERROR )
343     {
344         cWarning() << "Can't create transliterator";
345 
346         //it'll be checked later for non-ASCII characters
347         return input;
348     }
349 
350     icu::UnicodeString transliterable( input.utf16() );
351     transliterator->transliterate( transliterable );
352     return QString::fromUtf16( transliterable.getTerminatedBuffer() );
353 }
354 #else
355 static QString
transliterate(const QString & input)356 transliterate( const QString& input )
357 {
358     return input;
359 }
360 #endif
361 
362 static QString
makeLoginNameSuggestion(const QStringList & parts)363 makeLoginNameSuggestion( const QStringList& parts )
364 {
365     if ( parts.isEmpty() || parts.first().isEmpty() )
366     {
367         return QString();
368     }
369 
370     QString usernameSuggestion = parts.first();
371     for ( int i = 1; i < parts.length(); ++i )
372     {
373         if ( !parts.value( i ).isEmpty() )
374         {
375             usernameSuggestion.append( parts.value( i ).at( 0 ) );
376         }
377     }
378 
379     return USERNAME_RX.indexIn( usernameSuggestion ) != -1 ? usernameSuggestion : QString();
380 }
381 
382 static QString
makeHostnameSuggestion(const QStringList & parts)383 makeHostnameSuggestion( const QStringList& parts )
384 {
385     static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" );
386     if ( parts.isEmpty() || parts.first().isEmpty() )
387     {
388         return QString();
389     }
390 
391     QString productName = guessProductName();
392     QString hostnameSuggestion = QStringLiteral( "%1-%2" ).arg( parts.first() ).arg( productName );
393     return HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ? hostnameSuggestion : QString();
394 }
395 
396 void
setFullName(const QString & name)397 Config::setFullName( const QString& name )
398 {
399     CONFIG_PREVENT_EDITING( QString, "fullName" );
400 
401     if ( name.isEmpty() && !m_fullName.isEmpty() )
402     {
403         if ( !m_customHostName )
404         {
405             setHostName( name );
406         }
407         if ( !m_customLoginName )
408         {
409             setLoginName( name );
410         }
411         m_fullName = name;
412         emit fullNameChanged( name );
413     }
414 
415     if ( name != m_fullName )
416     {
417         m_fullName = name;
418         emit fullNameChanged( name );
419 
420         // Build login and hostname, if needed
421         static QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive );
422 
423         QString cleanName = CalamaresUtils::removeDiacritics( transliterate( name ) )
424                                 .replace( QRegExp( "[-']" ), "" )
425                                 .replace( rx, " " )
426                                 .toLower()
427                                 .simplified();
428 
429 
430         QStringList cleanParts = cleanName.split( ' ' );
431 
432         if ( !m_customLoginName )
433         {
434             QString login = makeLoginNameSuggestion( cleanParts );
435             if ( !login.isEmpty() && login != m_loginName )
436             {
437                 setLoginName( login );
438                 // It's **still** not custom, though setLoginName() sets that
439                 m_customLoginName = false;
440             }
441         }
442         if ( !m_customHostName )
443         {
444             QString hostname = makeHostnameSuggestion( cleanParts );
445             if ( !hostname.isEmpty() && hostname != m_hostName )
446             {
447                 setHostName( hostname );
448                 // Still not custom
449                 m_customHostName = false;
450             }
451         }
452     }
453 }
454 
455 void
setAutoLogin(bool b)456 Config::setAutoLogin( bool b )
457 {
458     if ( b != m_doAutoLogin )
459     {
460         m_doAutoLogin = b;
461         updateGSAutoLogin( b, loginName() );
462         emit autoLoginChanged( b );
463     }
464 }
465 
466 void
setReuseUserPasswordForRoot(bool reuse)467 Config::setReuseUserPasswordForRoot( bool reuse )
468 {
469     if ( reuse != m_reuseUserPasswordForRoot )
470     {
471         m_reuseUserPasswordForRoot = reuse;
472         emit reuseUserPasswordForRootChanged( reuse );
473         {
474             auto rp = rootPasswordStatus();
475             emit rootPasswordStatusChanged( rp.first, rp.second );
476         }
477     }
478 }
479 
480 void
setRequireStrongPasswords(bool strong)481 Config::setRequireStrongPasswords( bool strong )
482 {
483     if ( strong != m_requireStrongPasswords )
484     {
485         m_requireStrongPasswords = strong;
486         emit requireStrongPasswordsChanged( strong );
487         {
488             auto rp = rootPasswordStatus();
489             emit rootPasswordStatusChanged( rp.first, rp.second );
490         }
491         {
492             auto up = userPasswordStatus();
493             emit userPasswordStatusChanged( up.first, up.second );
494         }
495     }
496 }
497 
498 void
setUserPassword(const QString & s)499 Config::setUserPassword( const QString& s )
500 {
501     if ( s != m_userPassword )
502     {
503         m_userPassword = s;
504         const auto p = passwordStatus( m_userPassword, m_userPasswordSecondary );
505         emit userPasswordStatusChanged( p.first, p.second );
506         emit userPasswordChanged( s );
507     }
508 }
509 
510 void
setUserPasswordSecondary(const QString & s)511 Config::setUserPasswordSecondary( const QString& s )
512 {
513     if ( s != m_userPasswordSecondary )
514     {
515         m_userPasswordSecondary = s;
516         const auto p = passwordStatus( m_userPassword, m_userPasswordSecondary );
517         emit userPasswordStatusChanged( p.first, p.second );
518         emit userPasswordSecondaryChanged( s );
519     }
520 }
521 
522 /** @brief Checks two copies of the password for validity
523  *
524  * Given two copies of the password -- generally the password and
525  * the secondary fields -- checks them for validity and returns
526  * a pair of <validity, message>.
527  *
528  */
529 Config::PasswordStatus
passwordStatus(const QString & pw1,const QString & pw2) const530 Config::passwordStatus( const QString& pw1, const QString& pw2 ) const
531 {
532     if ( pw1 != pw2 )
533     {
534         return qMakePair( PasswordValidity::Invalid, tr( "Your passwords do not match!" ) );
535     }
536 
537     bool failureIsFatal = requireStrongPasswords();
538     for ( const auto& pc : m_passwordChecks )
539     {
540         QString message = pc.filter( pw1 );
541 
542         if ( !message.isEmpty() )
543         {
544             return qMakePair( failureIsFatal ? PasswordValidity::Invalid : PasswordValidity::Weak, message );
545         }
546     }
547 
548     return qMakePair( PasswordValidity::Valid, tr( "OK!" ) );
549 }
550 
551 
552 Config::PasswordStatus
userPasswordStatus() const553 Config::userPasswordStatus() const
554 {
555     return passwordStatus( m_userPassword, m_userPasswordSecondary );
556 }
557 
558 int
userPasswordValidity() const559 Config::userPasswordValidity() const
560 {
561     auto p = userPasswordStatus();
562     return p.first;
563 }
564 
565 QString
userPasswordMessage() const566 Config::userPasswordMessage() const
567 {
568     auto p = userPasswordStatus();
569     return p.second;
570 }
571 
572 
573 void
setRootPassword(const QString & s)574 Config::setRootPassword( const QString& s )
575 {
576     if ( writeRootPassword() && s != m_rootPassword )
577     {
578         m_rootPassword = s;
579         const auto p = passwordStatus( m_rootPassword, m_rootPasswordSecondary );
580         emit rootPasswordStatusChanged( p.first, p.second );
581         emit rootPasswordChanged( s );
582     }
583 }
584 
585 void
setRootPasswordSecondary(const QString & s)586 Config::setRootPasswordSecondary( const QString& s )
587 {
588     if ( writeRootPassword() && s != m_rootPasswordSecondary )
589     {
590         m_rootPasswordSecondary = s;
591         const auto p = passwordStatus( m_rootPassword, m_rootPasswordSecondary );
592         emit rootPasswordStatusChanged( p.first, p.second );
593         emit rootPasswordSecondaryChanged( s );
594     }
595 }
596 
597 QString
rootPassword() const598 Config::rootPassword() const
599 {
600     if ( writeRootPassword() )
601     {
602         if ( reuseUserPasswordForRoot() )
603         {
604             return userPassword();
605         }
606         return m_rootPassword;
607     }
608     return QString();
609 }
610 
611 QString
rootPasswordSecondary() const612 Config::rootPasswordSecondary() const
613 {
614     if ( writeRootPassword() )
615     {
616         if ( reuseUserPasswordForRoot() )
617         {
618             return userPasswordSecondary();
619         }
620         return m_rootPasswordSecondary;
621     }
622     return QString();
623 }
624 
625 Config::PasswordStatus
rootPasswordStatus() const626 Config::rootPasswordStatus() const
627 {
628     if ( writeRootPassword() && !reuseUserPasswordForRoot() )
629     {
630         return passwordStatus( m_rootPassword, m_rootPasswordSecondary );
631     }
632     else
633     {
634         return userPasswordStatus();
635     }
636 }
637 
638 int
rootPasswordValidity() const639 Config::rootPasswordValidity() const
640 {
641     auto p = rootPasswordStatus();
642     return p.first;
643 }
644 
645 QString
rootPasswordMessage() const646 Config::rootPasswordMessage() const
647 {
648     auto p = rootPasswordStatus();
649     return p.second;
650 }
651 
652 bool
isReady() const653 Config::isReady() const
654 {
655     bool readyFullName = !fullName().isEmpty();  // Needs some text
656     bool readyHostname = hostNameStatus().isEmpty();  // .. no warning message
657     bool readyUsername = !loginName().isEmpty() && loginNameStatus().isEmpty();  // .. no warning message
658     bool readyUserPassword = userPasswordValidity() != Config::PasswordValidity::Invalid;
659     bool readyRootPassword = rootPasswordValidity() != Config::PasswordValidity::Invalid;
660     return readyFullName && readyHostname && readyUsername && readyUserPassword && readyRootPassword;
661 }
662 
663 /** @brief Update ready status and emit signal
664  *
665  * This is a "concentrator" private slot for all the status-changed
666  * signals, so that readyChanged() is emitted only when needed.
667  */
668 void
checkReady()669 Config::checkReady()
670 {
671     bool b = isReady();
672     if ( b != m_isReady )
673     {
674         m_isReady = b;
675         emit readyChanged( b );
676     }
677 }
678 
679 
680 STATICTEST void
setConfigurationDefaultGroups(const QVariantMap & map,QList<GroupDescription> & defaultGroups)681 setConfigurationDefaultGroups( const QVariantMap& map, QList< GroupDescription >& defaultGroups )
682 {
683     defaultGroups.clear();
684 
685     const QString key( "defaultGroups" );
686     auto groupsFromConfig = map.value( key ).toList();
687     if ( groupsFromConfig.isEmpty() )
688     {
689         if ( map.contains( key ) && map.value( key ).isValid() && map.value( key ).canConvert( QVariant::List ) )
690         {
691             // Explicitly set, but empty: this is valid, but unusual.
692             cDebug() << key << "has explicit empty value.";
693         }
694         else
695         {
696             // By default give the user a handful of "traditional" groups, if
697             // none are specified at all. These are system (GID < 1000) groups.
698             cWarning() << "Using fallback groups. Please check *defaultGroups* value in users.conf";
699             for ( const auto& s : { "lp", "video", "network", "storage", "wheel", "audio" } )
700             {
701                 defaultGroups.append(
702                     GroupDescription( s, GroupDescription::CreateIfNeeded {}, GroupDescription::SystemGroup {} ) );
703             }
704         }
705     }
706     else
707     {
708         for ( const auto& v : groupsFromConfig )
709         {
710             if ( v.type() == QVariant::String )
711             {
712                 defaultGroups.append( GroupDescription( v.toString() ) );
713             }
714             else if ( v.type() == QVariant::Map )
715             {
716                 const auto innermap = v.toMap();
717                 QString name = CalamaresUtils::getString( innermap, "name" );
718                 if ( !name.isEmpty() )
719                 {
720                     defaultGroups.append( GroupDescription( name,
721                                                             CalamaresUtils::getBool( innermap, "must_exist", false ),
722                                                             CalamaresUtils::getBool( innermap, "system", false ) ) );
723                 }
724                 else
725                 {
726                     cWarning() << "Ignoring *defaultGroups* entry without a name" << v;
727                 }
728             }
729             else
730             {
731                 cWarning() << "Unknown *defaultGroups* entry" << v;
732             }
733         }
734     }
735 }
736 
737 STATICTEST HostNameActions
getHostNameActions(const QVariantMap & configurationMap)738 getHostNameActions( const QVariantMap& configurationMap )
739 {
740     HostNameAction setHostName = HostNameAction::EtcHostname;
741     QString hostnameActionString = CalamaresUtils::getString( configurationMap, "setHostname" );
742     if ( !hostnameActionString.isEmpty() )
743     {
744         bool ok = false;
745         setHostName = hostNameActionNames().find( hostnameActionString, ok );
746         if ( !ok )
747         {
748             setHostName = HostNameAction::EtcHostname;  // Rather than none
749         }
750     }
751 
752     HostNameAction writeHosts = CalamaresUtils::getBool( configurationMap, "writeHostsFile", true )
753         ? HostNameAction::WriteEtcHosts
754         : HostNameAction::None;
755     return setHostName | writeHosts;
756 }
757 
758 /** @brief Process entries in the passwordRequirements config entry
759  *
760  * Called once for each item in the config entry, which should
761  * be a key-value pair. What makes sense as a value depends on
762  * the key. Supported keys are documented in users.conf.
763  *
764  * @return if the check was added, returns @c true
765  */
766 STATICTEST bool
addPasswordCheck(const QString & key,const QVariant & value,PasswordCheckList & passwordChecks)767 addPasswordCheck( const QString& key, const QVariant& value, PasswordCheckList& passwordChecks )
768 {
769     if ( key == "minLength" )
770     {
771         add_check_minLength( passwordChecks, value );
772     }
773     else if ( key == "maxLength" )
774     {
775         add_check_maxLength( passwordChecks, value );
776     }
777     else if ( key == "nonempty" )
778     {
779         if ( value.toBool() )
780         {
781             passwordChecks.push_back(
782                 PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is empty" ); },
783                                []( const QString& s ) { return !s.isEmpty(); },
784                                PasswordCheck::Weight( 1 ) ) );
785         }
786         else
787         {
788             cDebug() << "nonempty check is mentioned but set to false";
789             return false;
790         }
791     }
792 #ifdef CHECK_PWQUALITY
793     else if ( key == "libpwquality" )
794     {
795         add_check_libpwquality( passwordChecks, value );
796     }
797 #endif  // CHECK_PWQUALITY
798     else
799     {
800         cWarning() << "Unknown password-check key" << key;
801         return false;
802     }
803     return true;
804 }
805 
806 /** @brief Returns a value of either key from the map
807  *
808  * Takes a function (e.g. getBool, or getString) and two keys,
809  * returning the value in the map of the one that is there (or @p defaultArg)
810  */
811 template < typename T, typename U >
812 T
813 either( T ( *f )( const QVariantMap&, const QString&, U ),
814         const QVariantMap& configurationMap,
815         const QString& oldKey,
816         const QString& newKey,
817         U defaultArg )
818 {
819     if ( configurationMap.contains( oldKey ) )
820     {
821         return f( configurationMap, oldKey, defaultArg );
822     }
823     else
824     {
825         return f( configurationMap, newKey, defaultArg );
826     }
827 }
828 
829 void
setConfigurationMap(const QVariantMap & configurationMap)830 Config::setConfigurationMap( const QVariantMap& configurationMap )
831 {
832     QString shell( QLatin1String( "/bin/bash" ) );  // as if it's not set at all
833     if ( configurationMap.contains( "userShell" ) )
834     {
835         shell = CalamaresUtils::getString( configurationMap, "userShell" );
836     }
837     // Now it might be explicitly set to empty, which is ok
838     setUserShell( shell );
839 
840     setAutoLoginGroup( either< QString, const QString& >(
841         CalamaresUtils::getString, configurationMap, "autologinGroup", "autoLoginGroup", QString() ) );
842     setSudoersGroup( CalamaresUtils::getString( configurationMap, "sudoersGroup" ) );
843 
844     m_hostNameActions = getHostNameActions( configurationMap );
845 
846     setConfigurationDefaultGroups( configurationMap, m_defaultGroups );
847 
848     // Renaming of Autologin -> AutoLogin in 4ffa79d4cf also affected
849     // configuration keys, which was not intended. Accept both.
850     m_doAutoLogin = either( CalamaresUtils::getBool,
851                             configurationMap,
852                             QStringLiteral( "doAutologin" ),
853                             QStringLiteral( "doAutoLogin" ),
854                             false );
855 
856     m_writeRootPassword = CalamaresUtils::getBool( configurationMap, "setRootPassword", true );
857     Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", m_writeRootPassword );
858 
859     m_reuseUserPasswordForRoot = CalamaresUtils::getBool( configurationMap, "doReusePassword", false );
860 
861     m_permitWeakPasswords = CalamaresUtils::getBool( configurationMap, "allowWeakPasswords", false );
862     m_requireStrongPasswords
863         = !m_permitWeakPasswords || !CalamaresUtils::getBool( configurationMap, "allowWeakPasswordsDefault", false );
864 
865     // If the value doesn't exist, or isn't a map, this gives an empty map -- no problem
866     auto pr_checks( configurationMap.value( "passwordRequirements" ).toMap() );
867     for ( decltype( pr_checks )::const_iterator i = pr_checks.constBegin(); i != pr_checks.constEnd(); ++i )
868     {
869         addPasswordCheck( i.key(), i.value(), m_passwordChecks );
870     }
871     std::sort( m_passwordChecks.begin(), m_passwordChecks.end() );
872 
873     updateGSAutoLogin( doAutoLogin(), loginName() );
874     checkReady();
875 
876     ApplyPresets( *this, configurationMap ) << "fullName"
877                                             << "loginName";
878 }
879 
880 void
finalizeGlobalStorage() const881 Config::finalizeGlobalStorage() const
882 {
883     updateGSAutoLogin( doAutoLogin(), loginName() );
884 
885     Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
886     if ( writeRootPassword() )
887     {
888         gs->insert( "reuseRootPassword", reuseUserPasswordForRoot() );
889     }
890     gs->insert( "password", CalamaresUtils::obscure( userPassword() ) );
891 }
892 
893 Calamares::JobList
createJobs() const894 Config::createJobs() const
895 {
896     Calamares::JobList jobs;
897 
898     if ( !isReady() )
899     {
900         return jobs;
901     }
902 
903     Calamares::Job* j;
904 
905     if ( !m_sudoersGroup.isEmpty() )
906     {
907         j = new SetupSudoJob( m_sudoersGroup );
908         jobs.append( Calamares::job_ptr( j ) );
909     }
910 
911     j = new SetupGroupsJob( this );
912     jobs.append( Calamares::job_ptr( j ) );
913 
914     j = new CreateUserJob( this );
915     jobs.append( Calamares::job_ptr( j ) );
916 
917     j = new SetPasswordJob( loginName(), userPassword() );
918     jobs.append( Calamares::job_ptr( j ) );
919 
920     j = new SetPasswordJob( "root", rootPassword() );
921     jobs.append( Calamares::job_ptr( j ) );
922 
923     j = new SetHostNameJob( hostName(), hostNameActions() );
924     jobs.append( Calamares::job_ptr( j ) );
925 
926     return jobs;
927 }
928