From e0c3bb3869637d332b98e77665e46293cb69d5ae Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 18 Jul 2025 05:53:31 +0000 Subject: [PATCH] Move root password menu into authentication menu (#3650) --- archinstall/lib/args.py | 19 +++++++--- .../lib/authentication/authentication_menu.py | 25 ++++++++++++ archinstall/lib/global_menu.py | 38 +++++++------------ archinstall/lib/models/authentication.py | 5 +++ archinstall/lib/models/users.py | 8 ++-- archinstall/scripts/guided.py | 4 +- tests/test_args.py | 11 ++++-- 7 files changed, 71 insertions(+), 39 deletions(-) diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index 4360879e..b255d1cc 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -23,6 +23,8 @@ from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.models.packages import Repository from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.users import Password, User +from archinstall.lib.models.profile_model import ProfileConfiguration +from archinstall.lib.models.users import Password, User, UserSerialization from archinstall.lib.output import debug, error, logger, warn from archinstall.lib.plugins import load_plugin from archinstall.lib.translationhandler import Language, tr, translation_handler @@ -78,14 +80,15 @@ class ArchConfig: # Special fields that should be handle with care due to security implications users: list[User] = field(default_factory=list) - root_enc_password: Password | None = None def unsafe_json(self) -> dict[str, Any]: - config = { + config: dict[str, list[UserSerialization] | str | None] = { 'users': [user.json() for user in self.users], - 'root_enc_password': self.root_enc_password.enc_password if self.root_enc_password else None, } + if self.auth_config and self.auth_config.root_enc_password: + config['root_enc_password'] = self.auth_config.root_enc_password.enc_password + if self.disk_config: disk_encryption = self.disk_config.disk_encryption if disk_encryption and disk_encryption.encryption_password: @@ -222,11 +225,17 @@ class ArchConfig: arch_config.services = services # DEPRECATED: backwards compatibility + root_password = None if root_password := args_config.get('!root-password', None): - arch_config.root_enc_password = Password(plaintext=root_password) + root_password = Password(plaintext=root_password) if enc_password := args_config.get('root_enc_password', None): - arch_config.root_enc_password = Password(enc_password=enc_password) + root_password = Password(enc_password=enc_password) + + if root_password is not None: + if arch_config.auth_config is None: + arch_config.auth_config = AuthenticationConfiguration() + arch_config.auth_config.root_enc_password = root_password if custom_commands := args_config.get('custom_commands', []): arch_config.custom_commands = custom_commands diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index b88853e4..9180e82f 100644 --- a/archinstall/lib/authentication/authentication_menu.py +++ b/archinstall/lib/authentication/authentication_menu.py @@ -3,7 +3,9 @@ from typing import override from archinstall.lib.disk.fido import Fido2 from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod +from archinstall.lib.models.users import Password from archinstall.lib.translationhandler import tr +from archinstall.lib.utils.util import get_password from archinstall.tui.curses_menu import SelectMenu from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType @@ -33,6 +35,12 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]): def _define_menu_options(self) -> list[MenuItem]: return [ + MenuItem( + text=tr('Root password'), + action=select_root_password, + preview_action=self._prev_root_pwd, + key='root_enc_password', + ), MenuItem( text=tr('U2F login setup'), action=select_u2f_login, @@ -42,6 +50,18 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]): ), ] + def _prev_root_pwd(self, item: MenuItem) -> str | None: + if item.value is not None: + password: Password = item.value + return f'{tr("Root password")}: {password.hidden()}' + return None + + def _depends_on_u2f(self) -> bool: + devices = Fido2.get_fido2_devices() + if not devices: + return False + return True + def _prev_u2f_login(self, item: MenuItem) -> str | None: if item.value is not None: u2f_config: U2FLoginConfiguration = item.value @@ -61,6 +81,11 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]): return None +def select_root_password(preset: str | None = None) -> Password | None: + password = get_password(text=tr('Root password'), allow_skip=True) + return password + + def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | None: devices = Fido2.get_fido2_devices() if not devices: diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 8014a643..e86e537c 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -34,10 +34,11 @@ from .models.network_configuration import NetworkConfiguration, NicType from .models.packages import Repository from .models.profile import ProfileConfiguration from .models.users import Password, User +from .models.profile_model import ProfileConfiguration +from .models.users import User from .output import FormattedOutput from .pacman.config import PacmanConfig from .translationhandler import Language, tr, translation_handler -from .utils.util import get_password class GlobalMenu(AbstractMenu[None]): @@ -110,16 +111,9 @@ class GlobalMenu(AbstractMenu[None]): preview_action=self._prev_hostname, key='hostname', ), - MenuItem( - text=tr('Root password'), - action=self._set_root_password, - preview_action=self._prev_root_pwd, - key='root_enc_password', - ), MenuItem( text=tr('Authentication'), action=self._select_authentication, - value=[], preview_action=self._prev_authentication, key='auth_config', ), @@ -230,13 +224,16 @@ class GlobalMenu(AbstractMenu[None]): missing = set() + item: MenuItem = self._item_group.find_by_key('auth_config') + auth_config: AuthenticationConfiguration | None = item.value + + if (auth_config is None or auth_config.root_enc_password is None) and not has_superuser(): + missing.add( + tr('Either root-password or at least 1 user with sudo privileges must be specified'), + ) + for item in self._item_group.items: - if item.key in ['root_enc_password', 'users']: - if not check('root_enc_password') and not has_superuser(): - missing.add( - tr('Either root-password or at least 1 user with sudo privileges must be specified'), - ) - elif item.mandatory: + if item.mandatory: assert item.key is not None if not check(item.key): missing.add(item.text) @@ -314,6 +311,9 @@ class GlobalMenu(AbstractMenu[None]): auth_config: AuthenticationConfiguration = item.value output = '' + if auth_config.root_enc_password: + output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n' + if auth_config.u2f_config: u2f_config = auth_config.u2f_config login_method = u2f_config.u2f_login_method.display_value() @@ -400,12 +400,6 @@ class GlobalMenu(AbstractMenu[None]): return f'{tr("Hostname")}: {item.value}' return None - def _prev_root_pwd(self, item: MenuItem) -> str | None: - if item.value is not None: - password: Password = item.value - return f'{tr("Root password")}: {password.hidden()}' - return None - def _prev_parallel_dw(self, item: MenuItem) -> str | None: if item.value is not None: return f'{tr("Parallel Downloads")}: {item.value}' @@ -510,10 +504,6 @@ class GlobalMenu(AbstractMenu[None]): return None - def _set_root_password(self, preset: str | None = None) -> Password | None: - password = get_password(text=tr('Root password'), allow_skip=True) - return password - def _select_disk_config( self, preset: DiskLayoutConfiguration | None = None, diff --git a/archinstall/lib/models/authentication.py b/archinstall/lib/models/authentication.py index 66831813..74ead43c 100644 --- a/archinstall/lib/models/authentication.py +++ b/archinstall/lib/models/authentication.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from enum import Enum from typing import Any, NotRequired, TypedDict +from archinstall.lib.models.users import Password from archinstall.lib.translationhandler import tr @@ -58,6 +59,7 @@ class U2FLoginConfiguration: @dataclass class AuthenticationConfiguration: + root_enc_password: Password | None = None u2f_config: U2FLoginConfiguration | None = None @staticmethod @@ -67,6 +69,9 @@ class AuthenticationConfiguration: if (u2f_config := args.get('u2f_config')) is not None: auth_config.u2f_config = U2FLoginConfiguration.parse_arg(u2f_config) + if enc_password := args.get('root_enc_password'): + auth_config.root_enc_password = Password(enc_password=enc_password) + return auth_config def json(self) -> AuthenticationSerialization: diff --git a/archinstall/lib/models/users.py b/archinstall/lib/models/users.py index e6a33841..3f18f52d 100644 --- a/archinstall/lib/models/users.py +++ b/archinstall/lib/models/users.py @@ -100,8 +100,8 @@ class PasswordStrength(Enum): return PasswordStrength.VERY_WEAK -_UserSerialization = TypedDict( - '_UserSerialization', +UserSerialization = TypedDict( + 'UserSerialization', { 'username': str, '!password': NotRequired[str], @@ -173,7 +173,7 @@ class User: 'groups': self.groups, } - def json(self) -> _UserSerialization: + def json(self) -> UserSerialization: return { 'username': self.username, 'enc_password': self.password.enc_password, @@ -184,7 +184,7 @@ class User: @classmethod def parse_arguments( cls, - args: list[_UserSerialization], + args: list[UserSerialization], ) -> list['User']: users: list[User] = [] diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 88a92978..6de7fe09 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -138,8 +138,8 @@ def perform_installation(mountpoint: Path) -> None: if accessibility_tools_in_use(): installation.enable_espeakup() - if root_pw := config.root_enc_password: - root_user = User('root', root_pw, False) + if config.auth_config and config.auth_config.root_enc_password: + root_user = User('root', config.auth_config.root_enc_password, False) installation.set_user_password(root_user) if (profile_config := config.profile_config) and profile_config.profile: diff --git a/tests/test_args.py b/tests/test_args.py index a8666be7..3feb0f9f 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -134,6 +134,7 @@ def test_config_file_parsing( audio_config=AudioConfiguration(audio=Audio.PIPEWIRE), ), auth_config=AuthenticationConfiguration( + root_enc_password=Password(enc_password='password_hash'), u2f_config=U2FLoginConfiguration( u2f_login_method=U2FLoginMethod.Passwordless, passwordless_sudo=True, @@ -223,7 +224,6 @@ def test_config_file_parsing( ), ], services=['service_1', 'service_2'], - root_enc_password=Password(enc_password='password_hash'), custom_commands=["echo 'Hello, World!'"], ) @@ -280,7 +280,8 @@ def test_deprecated_creds_config_parsing( handler = ArchConfigHandler() arch_config = handler.config - assert arch_config.root_enc_password == Password(plaintext='rootPwd') + assert arch_config.auth_config is not None + assert arch_config.auth_config.root_enc_password == Password(plaintext='rootPwd') assert arch_config.users == [ User( @@ -331,7 +332,8 @@ def test_encrypted_creds_with_arg( handler = ArchConfigHandler() arch_config = handler.config - assert arch_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7') + assert arch_config.auth_config is not None + assert arch_config.auth_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7') assert arch_config.users == [ User( username='t', @@ -359,7 +361,8 @@ def test_encrypted_creds_with_env_var( handler = ArchConfigHandler() arch_config = handler.config - assert arch_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7') + assert arch_config.auth_config is not None + assert arch_config.auth_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7') assert arch_config.users == [ User( username='t',