Add support for U2F authentication (#3638)
* Add U2F login support * Update * Update * Update * Update
This commit is contained in:
parent
b3b00aa00f
commit
55a383764f
|
|
@ -84,9 +84,14 @@ def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfigur
|
|||
allow_skip=True,
|
||||
).run()
|
||||
|
||||
enabled = result.item() == MenuItem.yes()
|
||||
|
||||
return BluetoothConfiguration(enabled)
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
enabled = result.item() == MenuItem.yes()
|
||||
return BluetoothConfiguration(enabled)
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from pydantic.dataclasses import dataclass as p_dataclass
|
|||
|
||||
from archinstall.lib.crypt import decrypt
|
||||
from archinstall.lib.models.application import ApplicationConfiguration
|
||||
from archinstall.lib.models.authentication import AuthenticationConfiguration
|
||||
from archinstall.lib.models.bootloader import Bootloader
|
||||
from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration
|
||||
from archinstall.lib.models.locale import LocaleConfiguration
|
||||
|
|
@ -64,6 +65,7 @@ class ArchConfig:
|
|||
bootloader: Bootloader = field(default=Bootloader.get_default())
|
||||
uki: bool = False
|
||||
app_config: ApplicationConfiguration | None = None
|
||||
auth_config: AuthenticationConfiguration | None = None
|
||||
hostname: str = 'archlinux'
|
||||
kernels: list[str] = field(default_factory=lambda: ['linux'])
|
||||
ntp: bool = True
|
||||
|
|
@ -107,6 +109,7 @@ class ArchConfig:
|
|||
'custom_commands': self.custom_commands,
|
||||
'bootloader': self.bootloader.json(),
|
||||
'app_config': self.app_config.json() if self.app_config else None,
|
||||
'auth_config': self.auth_config.json() if self.auth_config else None,
|
||||
}
|
||||
|
||||
if self.locale_config:
|
||||
|
|
@ -193,6 +196,9 @@ class ArchConfig:
|
|||
if audio_config_args is not None or app_config_args is not None:
|
||||
arch_config.app_config = ApplicationConfiguration.parse_arg(app_config_args, audio_config_args)
|
||||
|
||||
if auth_config_args := args_config.get('auth_config', None):
|
||||
arch_config.auth_config = AuthenticationConfiguration.parse_arg(auth_config_args)
|
||||
|
||||
if hostname := args_config.get('hostname', ''):
|
||||
arch_config.hostname = hostname
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
import getpass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from archinstall.lib.general import SysCommandWorker
|
||||
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
|
||||
from archinstall.lib.models.users import User
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import Tui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
||||
|
||||
class AuthenticationHandler:
|
||||
def __init__(self) -> None:
|
||||
self._u2f_auth_file = Path('etc/u2f_mappings')
|
||||
|
||||
def setup_auth(
|
||||
self,
|
||||
install_session: 'Installer',
|
||||
auth_config: AuthenticationConfiguration,
|
||||
users: list['User'],
|
||||
hostname: str,
|
||||
) -> None:
|
||||
if auth_config.u2f_config and users is not None:
|
||||
self._setup_u2f_login(install_session, auth_config.u2f_config, users, hostname)
|
||||
|
||||
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._update_pam_config(install_session, u2f_config)
|
||||
|
||||
def _update_pam_config(
|
||||
self,
|
||||
install_session: 'Installer',
|
||||
u2f_config: U2FLoginConfiguration,
|
||||
) -> None:
|
||||
match u2f_config.u2f_login_method:
|
||||
case U2FLoginMethod.Passwordless:
|
||||
config_entry = 'auth sufficient pam_u2f.so authfile=/etc/u2f_mappings cue'
|
||||
case U2FLoginMethod.SecondFactor:
|
||||
config_entry = 'auth required pam_u2f.so authfile=/etc/u2f_mappings cue'
|
||||
case _:
|
||||
raise ValueError(f'Unknown U2F login method: {u2f_config.u2f_login_method}')
|
||||
|
||||
debug(f'U2F PAM configuration: {config_entry}')
|
||||
debug(f'Passwordless sudo enabled: {u2f_config.passwordless_sudo}')
|
||||
|
||||
sudo_config = install_session.target / 'etc/pam.d/sudo'
|
||||
sys_login = install_session.target / 'etc/pam.d/system-login'
|
||||
|
||||
if u2f_config.passwordless_sudo:
|
||||
self._add_u2f_entry(sudo_config, config_entry)
|
||||
|
||||
self._add_u2f_entry(sys_login, config_entry)
|
||||
|
||||
def _add_u2f_entry(self, file: Path, entry: str) -> None:
|
||||
if not file.exists():
|
||||
debug(f'File does not exist: {file}')
|
||||
return None
|
||||
|
||||
content = file.read_text().splitlines()
|
||||
|
||||
# remove any existing u2f auth entry
|
||||
content = [line for line in content if 'pam_u2f.so' not in line]
|
||||
|
||||
# add the u2f auth entry as the first one after comments
|
||||
for i, line in enumerate(content):
|
||||
if not line.startswith('#'):
|
||||
content.insert(i, entry)
|
||||
break
|
||||
else:
|
||||
content.append(entry)
|
||||
|
||||
file.write_text('\n'.join(content) + '\n')
|
||||
|
||||
def _configure_u2f_mapping(
|
||||
self,
|
||||
install_session: 'Installer',
|
||||
u2f_config: U2FLoginConfiguration,
|
||||
users: list[User],
|
||||
hostname: str,
|
||||
) -> None:
|
||||
debug(f'Setting up U2F login: {u2f_config.u2f_login_method.value}')
|
||||
|
||||
install_session.pacman.strap('pam-u2f')
|
||||
|
||||
Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}'))
|
||||
|
||||
# https://developers.yubico.com/pam-u2f/
|
||||
u2f_auth_file = install_session.target / 'etc/u2f_mappings'
|
||||
u2f_auth_file.touch()
|
||||
existing_keys = u2f_auth_file.read_text()
|
||||
|
||||
registered_keys: list[str] = []
|
||||
|
||||
for user in users:
|
||||
Tui.print('')
|
||||
Tui.print(tr('Setting up U2F device for user: {}').format(user.username))
|
||||
Tui.print(tr('You may need to enter the PIN and then touch your U2F device to register it'))
|
||||
|
||||
cmd = ' '.join(['arch-chroot', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}'])
|
||||
|
||||
debug(f'Enrolling U2F device: {cmd}')
|
||||
|
||||
worker = SysCommandWorker(cmd, peek_output=True)
|
||||
pin_inputted = False
|
||||
|
||||
while worker.is_alive():
|
||||
if pin_inputted is False:
|
||||
if bytes('enter pin for', 'UTF-8') in worker._trace_log.lower():
|
||||
worker.write(bytes(getpass.getpass(''), 'UTF-8'))
|
||||
pin_inputted = True
|
||||
|
||||
output = worker.decode().strip().splitlines()
|
||||
debug(f'Output from pamu2fcfg: {output}')
|
||||
|
||||
key = output[-1].strip()
|
||||
registered_keys.append(key)
|
||||
|
||||
all_keys = '\n'.join(registered_keys)
|
||||
|
||||
if existing_keys:
|
||||
existing_keys += f'\n{all_keys}'
|
||||
else:
|
||||
existing_keys = all_keys
|
||||
|
||||
u2f_auth_file.write_text(existing_keys)
|
||||
|
||||
|
||||
auth_handler = AuthenticationHandler()
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
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.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
||||
|
||||
|
||||
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
||||
def __init__(self, preset: AuthenticationConfiguration | None = None):
|
||||
if preset:
|
||||
self._auth_config = preset
|
||||
else:
|
||||
self._auth_config = AuthenticationConfiguration()
|
||||
|
||||
menu_optioons = self._define_menu_options()
|
||||
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
|
||||
|
||||
super().__init__(
|
||||
self._item_group,
|
||||
config=self._auth_config,
|
||||
allow_reset=True,
|
||||
)
|
||||
|
||||
@override
|
||||
def run(self, additional_title: str | None = None) -> AuthenticationConfiguration:
|
||||
super().run(additional_title=additional_title)
|
||||
return self._auth_config
|
||||
|
||||
def _define_menu_options(self) -> list[MenuItem]:
|
||||
return [
|
||||
MenuItem(
|
||||
text=tr('U2F login setup'),
|
||||
action=setup_u2f_login,
|
||||
value=self._auth_config.u2f_config,
|
||||
preview_action=self._prev_u2f_login,
|
||||
dependencies=[self._depends_on_u2f],
|
||||
key='u2f_config',
|
||||
),
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
login_method = u2f_config.u2f_login_method.display_value()
|
||||
output = tr('U2F login method: ') + login_method
|
||||
|
||||
output += '\n'
|
||||
output += tr('Passwordless sudo: ') + (tr('Enabled') if u2f_config.passwordless_sudo else tr('Disabled'))
|
||||
|
||||
return output
|
||||
return None
|
||||
|
||||
|
||||
def setup_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | None:
|
||||
items = []
|
||||
for method in U2FLoginMethod:
|
||||
items.append(MenuItem(method.display_value(), value=method))
|
||||
|
||||
group = MenuItemGroup(items)
|
||||
|
||||
if preset is not None:
|
||||
group.set_selected_by_value(preset.u2f_login_method)
|
||||
|
||||
result = SelectMenu[U2FLoginMethod](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('U2F Login Method')),
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
u2f_method = result.get_value()
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.no()
|
||||
header = tr('Enable passwordless sudo?')
|
||||
|
||||
result_sudo = SelectMenu[bool](
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
|
||||
passwordless_sudo = result_sudo.item() == MenuItem.yes()
|
||||
|
||||
return U2FLoginConfiguration(
|
||||
u2f_login_method=u2f_method,
|
||||
passwordless_sudo=passwordless_sudo,
|
||||
)
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return None
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
|
@ -290,7 +290,7 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
|
|||
header = tr('Select a FIDO2 device to use for HSM') + '\n'
|
||||
|
||||
try:
|
||||
fido_devices = Fido2.get_fido2_devices()
|
||||
fido_devices = Fido2.get_cryptenroll_devices()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,39 @@ from ..output import error, info
|
|||
|
||||
|
||||
class Fido2:
|
||||
_loaded: bool = False
|
||||
_fido2_devices: ClassVar[list[Fido2Device]] = []
|
||||
_loaded_cryptsetup: bool = False
|
||||
_loaded_u2f: bool = False
|
||||
_cryptenroll_devices: ClassVar[list[Fido2Device]] = []
|
||||
_u2f_devices: ClassVar[list[Fido2Device]] = []
|
||||
|
||||
@classmethod
|
||||
def get_fido2_devices(cls, reload: bool = False) -> list[Fido2Device]:
|
||||
def get_fido2_devices(cls) -> list[Fido2Device]:
|
||||
"""
|
||||
fido2-tool output example:
|
||||
|
||||
/dev/hidraw4: vendor=0x1050, product=0x0407 (Yubico YubiKey OTP+FIDO+CCID)
|
||||
"""
|
||||
|
||||
if not cls._loaded_u2f:
|
||||
cls._loaded_u2f = True
|
||||
try:
|
||||
ret = SysCommand('fido2-token -L').decode()
|
||||
except SysCallError as e:
|
||||
error(f'failed to read fido2 devices: {e}')
|
||||
return []
|
||||
|
||||
fido_devices = clear_vt100_escape_codes_from_str(ret)
|
||||
|
||||
for line in fido_devices.split('\r\n'):
|
||||
path, details = line.replace(',', '').split(':', maxsplit=1)
|
||||
_, product, manufacturer = details.strip().split(' ', maxsplit=2)
|
||||
|
||||
cls._u2f_devices.append(Fido2Device(Path(path.strip()), manufacturer.strip(), product.strip().split('=')[1]))
|
||||
|
||||
return cls._u2f_devices
|
||||
|
||||
@classmethod
|
||||
def get_cryptenroll_devices(cls, reload: bool = False) -> list[Fido2Device]:
|
||||
"""
|
||||
Uses systemd-cryptenroll to list the FIDO2 devices
|
||||
connected that supports FIDO2.
|
||||
|
|
@ -38,7 +66,7 @@ class Fido2:
|
|||
|
||||
# to prevent continuous reloading which will slow
|
||||
# down moving the cursor in the menu
|
||||
if not cls._loaded or reload:
|
||||
if not cls._loaded_cryptsetup or reload:
|
||||
try:
|
||||
ret = SysCommand('systemd-cryptenroll --fido2-device=list').decode()
|
||||
except SysCallError:
|
||||
|
|
@ -65,10 +93,10 @@ class Fido2:
|
|||
Fido2Device(Path(path), manufacturer, product),
|
||||
)
|
||||
|
||||
cls._loaded = True
|
||||
cls._fido2_devices = devices
|
||||
cls._loaded_cryptsetup = True
|
||||
cls._cryptenroll_devices = devices
|
||||
|
||||
return cls._fido2_devices
|
||||
return cls._cryptenroll_devices
|
||||
|
||||
@classmethod
|
||||
def fido2_enroll(
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ from typing import override
|
|||
|
||||
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
|
||||
from archinstall.lib.models.application import ApplicationConfiguration
|
||||
from archinstall.lib.models.authentication import AuthenticationConfiguration
|
||||
from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification
|
||||
from archinstall.lib.packages import list_available_packages
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
|
||||
from .applications.application_menu import ApplicationMenu
|
||||
from .args import ArchConfig
|
||||
from .authentication.authentication_menu import AuthenticationMenu
|
||||
from .configuration import save_config
|
||||
from .hardware import SysInfo
|
||||
from .interactions.general_conf import (
|
||||
|
|
@ -114,6 +116,13 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
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',
|
||||
),
|
||||
MenuItem(
|
||||
text=tr('User account'),
|
||||
action=self._create_user_account,
|
||||
|
|
@ -257,6 +266,10 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
app_config = ApplicationMenu(preset).run()
|
||||
return app_config
|
||||
|
||||
def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
|
||||
auth_config = AuthenticationMenu(preset).run()
|
||||
return auth_config
|
||||
|
||||
def _update_lang_text(self) -> None:
|
||||
"""
|
||||
The options for the global menu are generated with a static text;
|
||||
|
|
@ -296,6 +309,23 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
return output
|
||||
return None
|
||||
|
||||
def _prev_authentication(self, item: MenuItem) -> str | None:
|
||||
if item.value:
|
||||
auth_config: AuthenticationConfiguration = item.value
|
||||
output = ''
|
||||
|
||||
if auth_config.u2f_config:
|
||||
u2f_config = auth_config.u2f_config
|
||||
login_method = u2f_config.u2f_login_method.display_value()
|
||||
output = tr('U2F login method: ') + login_method
|
||||
|
||||
output += '\n'
|
||||
output += tr('Passwordless sudo: ') + (tr('Enabled') if u2f_config.passwordless_sudo else tr('Disabled'))
|
||||
|
||||
return output
|
||||
|
||||
return None
|
||||
|
||||
def _prev_applications(self, item: MenuItem) -> str | None:
|
||||
if item.value:
|
||||
app_config: ApplicationConfiguration = item.value
|
||||
|
|
|
|||
|
|
@ -1506,7 +1506,7 @@ class Installer:
|
|||
|
||||
parent_dev_path = device_handler.get_parent_device_path(boot_partition.safe_dev_path)
|
||||
|
||||
cmd_template = (
|
||||
cmd_template = [
|
||||
'efibootmgr',
|
||||
'--create',
|
||||
'--disk',
|
||||
|
|
@ -1520,7 +1520,7 @@ class Installer:
|
|||
'--unicode',
|
||||
*cmdline,
|
||||
'--verbose',
|
||||
)
|
||||
]
|
||||
|
||||
for kernel in self.kernels:
|
||||
# Setup the firmware entry
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, NotRequired, Optional, TypedDict
|
||||
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
class U2FLoginConfigSerialization(TypedDict):
|
||||
u2f_login_method: str
|
||||
passwordless_sudo: bool
|
||||
|
||||
|
||||
class AuthenticationSerialization(TypedDict):
|
||||
u2f_config: NotRequired[U2FLoginConfigSerialization]
|
||||
|
||||
|
||||
class U2FLoginMethod(Enum):
|
||||
Passwordless = 'passwordless'
|
||||
SecondFactor = 'second_factor'
|
||||
|
||||
def display_value(self) -> str:
|
||||
match self:
|
||||
case U2FLoginMethod.Passwordless:
|
||||
return tr('Passwordless login')
|
||||
case U2FLoginMethod.SecondFactor:
|
||||
return tr('Second factor login')
|
||||
case _:
|
||||
raise ValueError(f'Unknown type: {self}')
|
||||
|
||||
|
||||
@dataclass
|
||||
class U2FLoginConfiguration:
|
||||
u2f_login_method: U2FLoginMethod
|
||||
passwordless_sudo: bool = False
|
||||
|
||||
def json(self) -> U2FLoginConfigSerialization:
|
||||
return {
|
||||
'u2f_login_method': self.u2f_login_method.value,
|
||||
'passwordless_sudo': self.passwordless_sudo,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_arg(args: dict[str, Any]) -> Optional['U2FLoginConfiguration']:
|
||||
u2f_login_method = args.get('u2f_login_method')
|
||||
|
||||
if u2f_login_method is None:
|
||||
return None
|
||||
|
||||
u2f_config = U2FLoginConfiguration(u2f_login_method=U2FLoginMethod(u2f_login_method))
|
||||
|
||||
u2f_config.u2f_login_method = U2FLoginMethod(u2f_login_method)
|
||||
|
||||
if passwordless_sudo := args.get('passwordless_sudo') is not None:
|
||||
u2f_config.passwordless_sudo = passwordless_sudo
|
||||
|
||||
return u2f_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthenticationConfiguration:
|
||||
u2f_config: U2FLoginConfiguration | None = None
|
||||
|
||||
@staticmethod
|
||||
def parse_arg(args: dict[str, Any]) -> 'AuthenticationConfiguration':
|
||||
auth_config = AuthenticationConfiguration()
|
||||
|
||||
if (u2f_config := args.get('u2f_config')) is not None:
|
||||
auth_config.u2f_config = U2FLoginConfiguration.parse_arg(u2f_config)
|
||||
|
||||
return auth_config
|
||||
|
||||
def json(self) -> AuthenticationSerialization:
|
||||
config: AuthenticationSerialization = {}
|
||||
|
||||
if self.u2f_config:
|
||||
config['u2f_config'] = self.u2f_config.json()
|
||||
|
||||
return config
|
||||
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
|||
from archinstall import SysInfo
|
||||
from archinstall.lib.applications.application_handler import application_handler
|
||||
from archinstall.lib.args import arch_config_handler
|
||||
from archinstall.lib.authentication.authentication_handler import auth_handler
|
||||
from archinstall.lib.configuration import ConfigurationOutput
|
||||
from archinstall.lib.disk.filesystem import FilesystemHandler
|
||||
from archinstall.lib.disk.utils import disk_layouts
|
||||
|
|
@ -116,6 +117,9 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
if users := config.users:
|
||||
installation.create_users(users)
|
||||
|
||||
if config.auth_config and config.users:
|
||||
auth_handler.setup_auth(installation, config.auth_config, config.users, config.hostname)
|
||||
|
||||
if config.packages and config.packages[0] != '':
|
||||
installation.add_additional_packages(config.packages)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@
|
|||
"audio": "pipewire"
|
||||
}
|
||||
},
|
||||
"auth_config": {
|
||||
"u2f_config": {
|
||||
"passwordless_sudo": true,
|
||||
"u2f_login_method": "passwordless"
|
||||
}
|
||||
},
|
||||
"audio_config": {
|
||||
"audio": "pipewire"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from archinstall.default_profiles.profile import GreeterType
|
|||
from archinstall.lib.args import ArchConfig, ArchConfigHandler, Arguments
|
||||
from archinstall.lib.hardware import GfxDriver
|
||||
from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration
|
||||
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
|
||||
from archinstall.lib.models.bootloader import Bootloader
|
||||
from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType
|
||||
from archinstall.lib.models.locale import LocaleConfiguration
|
||||
|
|
@ -132,6 +133,12 @@ def test_config_file_parsing(
|
|||
bluetooth_config=BluetoothConfiguration(enabled=True),
|
||||
audio_config=AudioConfiguration(audio=Audio.PIPEWIRE),
|
||||
),
|
||||
auth_config=AuthenticationConfiguration(
|
||||
u2f_config=U2FLoginConfiguration(
|
||||
u2f_login_method=U2FLoginMethod.Passwordless,
|
||||
passwordless_sudo=True,
|
||||
),
|
||||
),
|
||||
locale_config=LocaleConfiguration(
|
||||
kb_layout='us',
|
||||
sys_lang='en_US',
|
||||
|
|
|
|||
Loading…
Reference in New Issue