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