Move users menu into authentication submenu (#3678)

* Move users menu into authentication submenu

* Tests

* Update

* Update
This commit is contained in:
Daniel Girtler 2025-07-22 06:57:39 +00:00 committed by GitHub
parent 725c3fed09
commit 3e99cfbba7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 72 additions and 68 deletions

View File

@ -27,5 +27,6 @@ class DockerProfile(Profile):
def post_install(self, install_session: 'Installer') -> None: def post_install(self, install_session: 'Installer') -> None:
from archinstall.lib.args import arch_config_handler from archinstall.lib.args import arch_config_handler
for user in arch_config_handler.config.users: if auth_config := arch_config_handler.config.auth_config:
install_session.arch_chroot(f'usermod -a -G docker {user.username}') for user in auth_config.users:
install_session.arch_chroot(f'usermod -a -G docker {user.username}')

View File

@ -76,16 +76,15 @@ class ArchConfig:
services: list[str] = field(default_factory=list) services: list[str] = field(default_factory=list)
custom_commands: list[str] = field(default_factory=list) custom_commands: list[str] = field(default_factory=list)
# Special fields that should be handle with care due to security implications
users: list[User] = field(default_factory=list)
def unsafe_json(self) -> dict[str, Any]: def unsafe_json(self) -> dict[str, Any]:
config: dict[str, list[UserSerialization] | str | None] = { config: dict[str, list[UserSerialization] | str | None] = {}
'users': [user.json() for user in self.users],
}
if self.auth_config and self.auth_config.root_enc_password: if self.auth_config:
config['root_enc_password'] = self.auth_config.root_enc_password.enc_password if self.auth_config.users:
config['users'] = [user.json() for user in self.auth_config.users]
if self.auth_config.root_enc_password:
config['root_enc_password'] = self.auth_config.root_enc_password.enc_password
if self.disk_config: if self.disk_config:
disk_encryption = self.disk_config.disk_encryption disk_encryption = self.disk_config.disk_encryption
@ -177,13 +176,6 @@ class ArchConfig:
if net_config := args_config.get('network_config', None): if net_config := args_config.get('network_config', None):
arch_config.network_config = NetworkConfiguration.parse_arg(net_config) arch_config.network_config = NetworkConfiguration.parse_arg(net_config)
# DEPRECATED: backwards copatibility
if users := args_config.get('!users', None):
arch_config.users = User.parse_arguments(users)
if users := args_config.get('users', None):
arch_config.users = User.parse_arguments(users)
if bootloader_config := args_config.get('bootloader', None): if bootloader_config := args_config.get('bootloader', None):
arch_config.bootloader = Bootloader.from_arg(bootloader_config) arch_config.bootloader = Bootloader.from_arg(bootloader_config)
@ -235,6 +227,19 @@ class ArchConfig:
arch_config.auth_config = AuthenticationConfiguration() arch_config.auth_config = AuthenticationConfiguration()
arch_config.auth_config.root_enc_password = root_password arch_config.auth_config.root_enc_password = root_password
# DEPRECATED: backwards copatibility
users: list[User] = []
if args_users := args_config.get('!users', None):
users = User.parse_arguments(args_users)
if args_users := args_config.get('users', None):
users = User.parse_arguments(args_users)
if users:
if arch_config.auth_config is None:
arch_config.auth_config = AuthenticationConfiguration()
arch_config.auth_config.users = users
if custom_commands := args_config.get('custom_commands', []): if custom_commands := args_config.get('custom_commands', []):
arch_config.custom_commands = custom_commands arch_config.custom_commands = custom_commands

View File

@ -18,11 +18,10 @@ class AuthenticationHandler:
self, self,
install_session: 'Installer', install_session: 'Installer',
auth_config: AuthenticationConfiguration, auth_config: AuthenticationConfiguration,
users: list['User'],
hostname: str, hostname: str,
) -> None: ) -> None:
if auth_config.u2f_config and users is not None: if auth_config.u2f_config and auth_config.users is not None:
self._setup_u2f_login(install_session, auth_config.u2f_config, users, hostname) self._setup_u2f_login(install_session, auth_config.u2f_config, auth_config.users, hostname)
def _setup_u2f_login(self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User], hostname: str) -> None: def _setup_u2f_login(self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User], hostname: str) -> None:
self._configure_u2f_mapping(install_session, u2f_config, users, hostname) self._configure_u2f_mapping(install_session, u2f_config, users, hostname)

View File

@ -1,9 +1,11 @@
from typing import override from typing import override
from archinstall.lib.disk.fido import Fido2 from archinstall.lib.disk.fido import Fido2
from archinstall.lib.interactions.manage_users_conf import ask_for_additional_users
from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import Password from archinstall.lib.models.users import Password, User
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.util import get_password from archinstall.lib.utils.util import get_password
from archinstall.tui.curses_menu import SelectMenu from archinstall.tui.curses_menu import SelectMenu
@ -41,6 +43,12 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
preview_action=self._prev_root_pwd, preview_action=self._prev_root_pwd,
key='root_enc_password', key='root_enc_password',
), ),
MenuItem(
text=tr('User account'),
action=self._create_user_account,
preview_action=self._prev_users,
key='users',
),
MenuItem( MenuItem(
text=tr('U2F login setup'), text=tr('U2F login setup'),
action=select_u2f_login, action=select_u2f_login,
@ -50,6 +58,18 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
), ),
] ]
def _create_user_account(self, preset: list[User] | None = None) -> list[User]:
preset = [] if preset is None else preset
users = ask_for_additional_users(defined_users=preset)
return users
def _prev_users(self, item: MenuItem) -> str | None:
users: list[User] | None = item.value
if users:
return FormattedOutput.as_table(users)
return None
def _prev_root_pwd(self, item: MenuItem) -> str | None: def _prev_root_pwd(self, item: MenuItem) -> str | None:
if item.value is not None: if item.value is not None:
password: Password = item.value password: Password = item.value

View File

@ -21,7 +21,6 @@ from .interactions.general_conf import (
ask_hostname, ask_hostname,
ask_ntp, ask_ntp,
) )
from .interactions.manage_users_conf import ask_for_additional_users
from .interactions.network_menu import ask_to_configure_network from .interactions.network_menu import ask_to_configure_network
from .interactions.system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_kernel from .interactions.system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_kernel
from .locale.locale_menu import LocaleMenu from .locale.locale_menu import LocaleMenu
@ -33,7 +32,6 @@ from .models.mirrors import MirrorConfiguration
from .models.network import NetworkConfiguration, NicType from .models.network import NetworkConfiguration, NicType
from .models.packages import Repository from .models.packages import Repository
from .models.profile import ProfileConfiguration from .models.profile import ProfileConfiguration
from .models.users import User
from .output import FormattedOutput from .output import FormattedOutput
from .pacman.config import PacmanConfig from .pacman.config import PacmanConfig
from .translationhandler import Language, tr, translation_handler from .translationhandler import Language, tr, translation_handler
@ -115,12 +113,6 @@ class GlobalMenu(AbstractMenu[None]):
preview_action=self._prev_authentication, preview_action=self._prev_authentication,
key='auth_config', key='auth_config',
), ),
MenuItem(
text=tr('User account'),
action=self._create_user_account,
preview_action=self._prev_users,
key='users',
),
MenuItem( MenuItem(
text=tr('Profile'), text=tr('Profile'),
action=self._select_profile, action=self._select_profile,
@ -207,24 +199,20 @@ class GlobalMenu(AbstractMenu[None]):
save_config(self._arch_config) save_config(self._arch_config)
def _missing_configs(self) -> list[str]: def _missing_configs(self) -> list[str]:
item: MenuItem = self._item_group.find_by_key('auth_config')
auth_config: AuthenticationConfiguration | None = item.value
def check(s: str) -> bool: def check(s: str) -> bool:
item = self._item_group.find_by_key(s) item = self._item_group.find_by_key(s)
return item.has_value() return item.has_value()
def has_superuser() -> bool: def has_superuser() -> bool:
item = self._item_group.find_by_key('users') if auth_config and auth_config.users:
return any([u.sudo for u in auth_config.users])
if item.has_value():
users = item.value
if users:
return any([u.sudo for u in users])
return False return False
missing = set() 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(): if (auth_config is None or auth_config.root_enc_password is None) and not has_superuser():
missing.add( missing.add(
tr('Either root-password or at least 1 user with sudo privileges must be specified'), tr('Either root-password or at least 1 user with sudo privileges must be specified'),
@ -312,6 +300,9 @@ class GlobalMenu(AbstractMenu[None]):
if auth_config.root_enc_password: if auth_config.root_enc_password:
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n' output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
if auth_config.users:
output += FormattedOutput.as_table(auth_config.users) + '\n'
if auth_config.u2f_config: if auth_config.u2f_config:
u2f_config = auth_config.u2f_config u2f_config = auth_config.u2f_config
login_method = u2f_config.u2f_login_method.display_value() login_method = u2f_config.u2f_login_method.display_value()
@ -475,13 +466,6 @@ class GlobalMenu(AbstractMenu[None]):
return None return None
def _prev_users(self, item: MenuItem) -> str | None:
users: list[User] | None = item.value
if users:
return FormattedOutput.as_table(users)
return None
def _prev_profile(self, item: MenuItem) -> str | None: def _prev_profile(self, item: MenuItem) -> str | None:
profile_config: ProfileConfiguration | None = item.value profile_config: ProfileConfiguration | None = item.value
@ -543,11 +527,6 @@ class GlobalMenu(AbstractMenu[None]):
return packages return packages
def _create_user_account(self, preset: list[User] | None = None) -> list[User]:
preset = [] if preset is None else preset
users = ask_for_additional_users(defined_users=preset)
return users
def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration: def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration:
mirror_configuration = MirrorMenu(preset=preset).run() mirror_configuration = MirrorMenu(preset=preset).run()

View File

@ -1,8 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Any, NotRequired, TypedDict from typing import Any, NotRequired, TypedDict
from archinstall.lib.models.users import Password from archinstall.lib.models.users import Password, User
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -60,6 +60,7 @@ class U2FLoginConfiguration:
@dataclass @dataclass
class AuthenticationConfiguration: class AuthenticationConfiguration:
root_enc_password: Password | None = None root_enc_password: Password | None = None
users: list[User] = field(default_factory=list)
u2f_config: U2FLoginConfiguration | None = None u2f_config: U2FLoginConfiguration | None = None
@staticmethod @staticmethod

View File

@ -114,11 +114,10 @@ def perform_installation(mountpoint: Path) -> None:
config.profile_config, config.profile_config,
) )
if users := config.users: if config.auth_config:
installation.create_users(users) if config.auth_config.users:
installation.create_users(config.auth_config.users)
if config.auth_config and config.users: auth_handler.setup_auth(installation, config.auth_config, config.hostname)
auth_handler.setup_auth(installation, config.auth_config, config.users, config.hostname)
if config.packages and config.packages[0] != '': if config.packages and config.packages[0] != '':
installation.add_additional_packages(config.packages) installation.add_additional_packages(config.packages)

View File

@ -135,6 +135,14 @@ def test_config_file_parsing(
), ),
auth_config=AuthenticationConfiguration( auth_config=AuthenticationConfiguration(
root_enc_password=Password(enc_password='password_hash'), root_enc_password=Password(enc_password='password_hash'),
users=[
User(
username='user_name',
password=Password(enc_password='password_hash'),
sudo=True,
groups=['wheel'],
),
],
u2f_config=U2FLoginConfiguration( u2f_config=U2FLoginConfiguration(
u2f_login_method=U2FLoginMethod.Passwordless, u2f_login_method=U2FLoginMethod.Passwordless,
passwordless_sudo=True, passwordless_sudo=True,
@ -215,14 +223,6 @@ def test_config_file_parsing(
parallel_downloads=66, parallel_downloads=66,
swap=False, swap=False,
timezone='UTC', timezone='UTC',
users=[
User(
username='user_name',
password=Password(enc_password='password_hash'),
sudo=True,
groups=['wheel'],
),
],
services=['service_1', 'service_2'], services=['service_1', 'service_2'],
custom_commands=["echo 'Hello, World!'"], custom_commands=["echo 'Hello, World!'"],
) )
@ -283,7 +283,7 @@ def test_deprecated_creds_config_parsing(
assert arch_config.auth_config is not None assert arch_config.auth_config is not None
assert arch_config.auth_config.root_enc_password == Password(plaintext='rootPwd') assert arch_config.auth_config.root_enc_password == Password(plaintext='rootPwd')
assert arch_config.users == [ assert arch_config.auth_config.users == [
User( User(
username='user_name', username='user_name',
password=Password(plaintext='userPwd'), password=Password(plaintext='userPwd'),
@ -334,7 +334,7 @@ def test_encrypted_creds_with_arg(
assert arch_config.auth_config is not None 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.auth_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7')
assert arch_config.users == [ assert arch_config.auth_config.users == [
User( User(
username='t', username='t',
password=Password(enc_password='$y$j9T$3KxMigAEnjtzbjalhLewE.$gmuoQtc9RNY/PmO/GxHHYvkZNO86Eeftg1Oc7L.QSO/'), password=Password(enc_password='$y$j9T$3KxMigAEnjtzbjalhLewE.$gmuoQtc9RNY/PmO/GxHHYvkZNO86Eeftg1Oc7L.QSO/'),
@ -363,7 +363,7 @@ def test_encrypted_creds_with_env_var(
assert arch_config.auth_config is not None 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.auth_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7')
assert arch_config.users == [ assert arch_config.auth_config.users == [
User( User(
username='t', username='t',
password=Password(enc_password='$y$j9T$3KxMigAEnjtzbjalhLewE.$gmuoQtc9RNY/PmO/GxHHYvkZNO86Eeftg1Oc7L.QSO/'), password=Password(enc_password='$y$j9T$3KxMigAEnjtzbjalhLewE.$gmuoQtc9RNY/PmO/GxHHYvkZNO86Eeftg1Oc7L.QSO/'),