Rework user management (#1220)

* Rework users

* Update user installation

* Fix config serialization

* Update

* Update schemas and documentation

* Update

* Fix flake8

* Make users mypy compatible

* Fix minor copy

Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com>
Co-authored-by: Anton Hvornum <anton@hvornum.se>
This commit is contained in:
Daniel Girtler 2022-05-27 05:48:29 +10:00 committed by GitHub
parent 353c05318c
commit 870da403e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 287 additions and 222 deletions

View File

@ -15,4 +15,4 @@ jobs:
# one day this will be enabled # one day this will be enabled
# run: mypy --strict --module archinstall || exit 0 # run: mypy --strict --module archinstall || exit 0
- name: run mypy - name: run mypy
run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py

View File

@ -105,7 +105,8 @@ with archinstall.Installer('/mnt') as installation:
# In this case, we install a minimal profile that is empty # In this case, we install a minimal profile that is empty
installation.install_profile('minimal') installation.install_profile('minimal')
installation.user_create('devel', 'devel') user = User('devel', 'devel', False)
installation.create_users(user)
installation.user_set_pw('root', 'airoot') installation.user_set_pw('root', 'airoot')
``` ```

View File

@ -1,7 +1,4 @@
"""Arch Linux installer - guided, templates etc.""" """Arch Linux installer - guided, templates etc."""
import urllib.error
import urllib.parse
import urllib.request
from argparse import ArgumentParser from argparse import ArgumentParser
from .lib.disk import * from .lib.disk import *
@ -13,6 +10,7 @@ from .lib.locale_helpers import *
from .lib.luks import * from .lib.luks import *
from .lib.mirrors import * from .lib.mirrors import *
from .lib.models.network_configuration import NetworkConfigurationHandler from .lib.models.network_configuration import NetworkConfigurationHandler
from .lib.models.users import User
from .lib.networking import * from .lib.networking import *
from .lib.output import * from .lib.output import *
from .lib.models.dataclasses import ( from .lib.models.dataclasses import (
@ -211,6 +209,11 @@ def load_config():
handler = NetworkConfigurationHandler() handler = NetworkConfigurationHandler()
handler.parse_arguments(arguments.get('nic')) handler.parse_arguments(arguments.get('nic'))
arguments['nic'] = handler.configuration arguments['nic'] = handler.configuration
if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None:
users = arguments.get('!users', None)
superusers = arguments.get('!superusers', None)
arguments['!users'] = User.parse_arguments(users, superusers)
def post_process_arguments(arguments): def post_process_arguments(arguments):
storage['arguments'] = arguments storage['arguments'] = arguments

View File

@ -37,7 +37,7 @@ class ConfigurationOutput:
self._user_creds_file = "user_credentials.json" self._user_creds_file = "user_credentials.json"
self._disk_layout_file = "user_disk_layout.json" self._disk_layout_file = "user_disk_layout.json"
self._sensitive = ['!users', '!superusers', '!encryption-password'] self._sensitive = ['!users', '!encryption-password']
self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run'] self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run']
self._process_config() self._process_config()

View File

@ -23,6 +23,7 @@ from .profiles import Profile
from .disk.partition import get_mount_fs_type from .disk.partition import get_mount_fs_type
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError
from .hsm import fido2_enroll from .hsm import fido2_enroll
from .models.users import User
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -1062,7 +1063,7 @@ class Installer:
self.log(f'Installing archinstall profile {profile}', level=logging.INFO) self.log(f'Installing archinstall profile {profile}', level=logging.INFO)
return profile.install() return profile.install()
def enable_sudo(self, entity: str, group :bool = False) -> bool: def enable_sudo(self, entity: str, group :bool = False):
self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO) self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO)
sudoers_dir = f"{self.target}/etc/sudoers.d" sudoers_dir = f"{self.target}/etc/sudoers.d"
@ -1092,9 +1093,14 @@ class Installer:
# Guarantees sudoer conf file recommended perms # Guarantees sudoer conf file recommended perms
os.chmod(pathlib.Path(rule_file_name), 0o440) os.chmod(pathlib.Path(rule_file_name), 0o440)
return True def create_users(self, users: Union[User, List[User]]):
if not isinstance(users, list):
users = [users]
def user_create(self, user :str, password :Optional[str] = None, groups :Optional[str] = None, sudo :bool = False) -> None: for user in users:
self.user_create(user.username, user.password, user.groups, user.sudo)
def user_create(self, user :str, password :Optional[str] = None, groups :Optional[List[str]] = None, sudo :bool = False) -> None:
if groups is None: if groups is None:
groups = [] groups = []

View File

@ -21,7 +21,6 @@ from ..user_interaction import ask_hostname
from ..user_interaction import ask_for_audio_selection from ..user_interaction import ask_for_audio_selection
from ..user_interaction import ask_additional_packages_to_install from ..user_interaction import ask_additional_packages_to_install
from ..user_interaction import ask_to_configure_network from ..user_interaction import ask_to_configure_network
from ..user_interaction import ask_for_superuser_account
from ..user_interaction import ask_for_additional_users from ..user_interaction import ask_for_additional_users
from ..user_interaction import select_language from ..user_interaction import select_language
from ..user_interaction import select_mirror_regions from ..user_interaction import select_mirror_regions
@ -33,7 +32,9 @@ from ..user_interaction import select_encrypted_partitions
from ..user_interaction import select_harddrives from ..user_interaction import select_harddrives
from ..user_interaction import select_profile from ..user_interaction import select_profile
from ..user_interaction import select_additional_repositories from ..user_interaction import select_additional_repositories
from ..models.users import User
from ..user_interaction.partitioning_conf import current_partition_layout from ..user_interaction.partitioning_conf import current_partition_layout
from ..output import FormattedOutput
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -122,21 +123,13 @@ class GlobalMenu(GeneralMenu):
_('Root password'), _('Root password'),
lambda preset:self._set_root_password(), lambda preset:self._set_root_password(),
display_func=lambda x: secret(x) if x else 'None') display_func=lambda x: secret(x) if x else 'None')
self._menu_options['!superusers'] = \
Selector(
_('Superuser account'),
lambda preset: self._create_superuser_account(),
default={},
exec_func=lambda n,v:self._users_resynch(),
dependencies_not=['!root-password'],
display_func=lambda x: self._display_superusers())
self._menu_options['!users'] = \ self._menu_options['!users'] = \
Selector( Selector(
_('User account'), _('User account'),
lambda x: self._create_user_account(), lambda x: self._create_user_account(x),
default={}, default={},
exec_func=lambda n,v:self._users_resynch(), display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else None,
display_func=lambda x: list(x.keys()) if x else '[]') preview_func=self._prev_users)
self._menu_options['profile'] = \ self._menu_options['profile'] = \
Selector( Selector(
_('Profile'), _('Profile'),
@ -273,17 +266,28 @@ class GlobalMenu(GeneralMenu):
return text[:-1] # remove last new line return text[:-1] # remove last new line
return None return None
def _prev_users(self) -> Optional[str]:
selector = self._menu_options['!users']
if selector.has_selection():
users: List[User] = selector.current_selection
return FormattedOutput.as_table(users)
return None
def _missing_configs(self) -> List[str]: def _missing_configs(self) -> List[str]:
def check(s): def check(s):
return self._menu_options.get(s).has_selection() return self._menu_options.get(s).has_selection()
def has_superuser() -> bool:
users = self._menu_options['!users'].current_selection
return any([u.sudo for u in users])
missing = [] missing = []
if not check('bootloader'): if not check('bootloader'):
missing += ['Bootloader'] missing += ['Bootloader']
if not check('hostname'): if not check('hostname'):
missing += ['Hostname'] missing += ['Hostname']
if not check('!root-password') and not check('!superusers'): if not check('!root-password') and not has_superuser():
missing += [str(_('Either root-password or at least 1 superuser must be specified'))] missing += [str(_('Either root-password or at least 1 user with sudo privileges must be specified'))]
if not check('harddrives'): if not check('harddrives'):
missing += ['Hard drives'] missing += ['Hard drives']
if check('harddrives'): if check('harddrives'):
@ -380,23 +384,6 @@ class GlobalMenu(GeneralMenu):
return ret return ret
def _create_superuser_account(self) -> Optional[Dict[str, Dict[str, str]]]: def _create_user_account(self, defined_users: List[User]) -> List[User]:
superusers = ask_for_superuser_account(str(_('Manage superuser accounts: '))) users = ask_for_additional_users(defined_users=defined_users)
return superusers if superusers else None
def _create_user_account(self) -> Dict[str, Dict[str, str | None]]:
users = ask_for_additional_users(str(_('Manage ordinary user accounts: ')))
return users return users
def _display_superusers(self):
superusers = self._data_store.get('!superusers', {})
if self._menu_options.get('!root-password').has_selection():
return list(superusers.keys()) if superusers else '[]'
else:
return list(superusers.keys()) if superusers else ''
def _users_resynch(self) -> bool:
self.synch('!superusers')
self.synch('!users')
return False

View File

@ -84,12 +84,12 @@ The contents in the base class of this methods serve for a very basic usage, and
``` ```
""" """
import copy
from os import system
from typing import Union, Any, TYPE_CHECKING, Dict, Optional
from .text_input import TextInput from .text_input import TextInput
from .menu import Menu, MenuSelectionType from .menu import Menu
from os import system
from copy import copy
from typing import Union, Any, TYPE_CHECKING, Dict, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -144,14 +144,14 @@ class ListManager:
self.bottom_list = [self.confirm_action,self.cancel_action] self.bottom_list = [self.confirm_action,self.cancel_action]
self.bottom_item = [self.cancel_action] self.bottom_item = [self.cancel_action]
self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))] self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))]
self.base_data = base_list self._original_data = copy.deepcopy(base_list)
self._data = copy(base_list) # as refs, changes are immediate self._data = copy.deepcopy(base_list) # as refs, changes are immediate
# default values for the null case # default values for the null case
self.target: Optional[Any] = None self.target: Optional[Any] = None
self.action = self._null_action self.action = self._null_action
if len(self._data) == 0 and self._null_action: if len(self._data) == 0 and self._null_action:
self.exec_action(self._data) self._data = self.exec_action(self._data)
def run(self): def run(self):
while True: while True:
@ -175,12 +175,10 @@ class ListManager:
clear_screen=False, clear_screen=False,
clear_menu_on_exit=False, clear_menu_on_exit=False,
header=self.header, header=self.header,
skip_empty_entries=True skip_empty_entries=True,
skip=False
).run() ).run()
if target.type_ == MenuSelectionType.Esc:
return self.run()
if not target.value or target.value in self.bottom_list: if not target.value or target.value in self.bottom_list:
self.action = target self.action = target
break break
@ -188,21 +186,23 @@ class ListManager:
if target.value and target.value in self._default_action: if target.value and target.value in self._default_action:
self.action = target.value self.action = target.value
self.target = None self.target = None
self.exec_action(self._data) self._data = self.exec_action(self._data)
continue continue
if isinstance(self._data,dict): if isinstance(self._data,dict):
data_key = data_formatted[target.value] data_key = data_formatted[target.value]
key = self._data[data_key] key = self._data[data_key]
self.target = {data_key: key} self.target = {data_key: key}
elif isinstance(self._data, list):
self.target = [d for d in self._data if d == data_formatted[target.value]][0]
else: else:
self.target = self._data[data_formatted[target.value]] self.target = self._data[data_formatted[target.value]]
# Possible enhacement. If run_actions returns false a message line indicating the failure # Possible enhacement. If run_actions returns false a message line indicating the failure
self.run_actions(target.value) self.run_actions(target.value)
if not target or target == self.cancel_action: # TODO dubious if target.value == self.cancel_action: # TODO dubious
return self.base_data # return the original list return self._original_data # return the original list
else: else:
return self._data return self._data
@ -221,10 +221,9 @@ class ListManager:
self.action = choice.value self.action = choice.value
if not self.action or self.action == self.cancel_action: if self.action and self.action != self.cancel_action:
return False self._data = self.exec_action(self._data)
else:
return self.exec_action(self._data)
""" """
The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case
""" """
@ -293,3 +292,5 @@ class ListManager:
self._data[origkey] = value self._data[origkey] = value
elif self.action == str(_('Delete')): elif self.action == str(_('Delete')):
del self._data[origkey] del self._data[origkey]
return self._data

View File

@ -0,0 +1,77 @@
from dataclasses import dataclass
from typing import Dict, List, Union, Any, TYPE_CHECKING
if TYPE_CHECKING:
_: Any
@dataclass
class User:
username: str
password: str
sudo: bool
@property
def groups(self) -> List[str]:
# this property should be transferred into a class attr instead
# if it's every going to be used
return []
def json(self) -> Dict[str, Any]:
return {
'username': self.username,
'!password': self.password,
'sudo': self.sudo
}
def display(self) -> str:
password = '*' * len(self.password)
return f'{_("Username")}: {self.username:16} {_("Password")}: {password:16} sudo: {str(self.sudo)}'
@classmethod
def _parse(cls, config_users: List[Dict[str, Any]]) -> List['User']:
users = []
for entry in config_users:
username = entry.get('username', None)
password = entry.get('!password', '')
sudo = entry.get('sudo', False)
if username is None:
continue
user = User(username, password, sudo)
users.append(user)
return users
@classmethod
def _parse_backwards_compatible(cls, config_users: Dict, sudo: bool) -> List['User']:
if len(config_users.keys()) > 0:
username = list(config_users.keys())[0]
password = config_users[username]['!password']
if password:
return [User(username, password, sudo)]
return []
@classmethod
def parse_arguments(
cls,
config_users: Union[List[Dict[str, str]], Dict[str, str]],
config_superusers: Union[List[Dict[str, str]], Dict[str, str]]
) -> List['User']:
users = []
# backwards compability
if isinstance(config_users, dict):
users += cls._parse_backwards_compatible(config_users, False)
else:
users += cls._parse(config_users)
# backwards compability
if isinstance(config_superusers, dict):
users += cls._parse_backwards_compatible(config_superusers, True)
return users

View File

@ -2,11 +2,47 @@ import logging
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Dict, Union from typing import Dict, Union, List, Any
from .storage import storage from .storage import storage
class FormattedOutput:
@classmethod
def values(cls, o: Any) -> Dict[str, Any]:
if hasattr(o, 'json'):
return o.json()
else:
return o.__dict__
@classmethod
def as_table(cls, obj: List[Any]) -> str:
column_width: Dict[str, int] = {}
for o in obj:
for k, v in cls.values(o).items():
column_width.setdefault(k, 0)
column_width[k] = max([column_width[k], len(str(v)), len(k)])
output = ''
for key, width in column_width.items():
key = key.replace('!', '')
output += key.ljust(width) + ' | '
output = output[:-3] + '\n'
output += '-' * len(output) + '\n'
for o in obj:
for k, v in cls.values(o).items():
if '!' in k:
v = '*' * len(str(v))
output += str(v).ljust(column_width[k]) + ' | '
output = output[:-3]
output += '\n'
return output
class Journald: class Journald:
@staticmethod @staticmethod
def log(message :str, level :int = logging.DEBUG) -> None: def log(message :str, level :int = logging.DEBUG) -> None:

View File

@ -1,5 +1,5 @@
from .save_conf import save_config from .save_conf import save_config
from .manage_users_conf import ask_for_superuser_account, ask_for_additional_users from .manage_users_conf import ask_for_additional_users
from .backwards_compatible_conf import generic_select, generic_multi_select from .backwards_compatible_conf import generic_select, generic_multi_select
from .locale_conf import select_locale_lang, select_locale_enc from .locale_conf import select_locale_lang, select_locale_enc
from .system_conf import select_kernel, select_harddrives, select_driver, ask_for_bootloader, ask_for_swap from .system_conf import select_kernel, select_harddrives, select_driver, ask_for_bootloader, ask_for_swap

View File

@ -1,14 +1,12 @@
from __future__ import annotations from __future__ import annotations
import logging
import re import re
from typing import Any, Dict, TYPE_CHECKING, List from typing import Any, Dict, TYPE_CHECKING, List, Optional
from .utils import get_password
from ..menu import Menu from ..menu import Menu
from ..menu.list_manager import ListManager from ..menu.list_manager import ListManager
from ..output import log from ..models.users import User
from ..storage import storage
from .utils import get_password
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -19,7 +17,7 @@ class UserList(ListManager):
subclass of ListManager for the managing of user accounts subclass of ListManager for the managing of user accounts
""" """
def __init__(self, prompt: str, lusers: dict, sudo: bool = None): def __init__(self, prompt: str, lusers: List[User]):
""" """
param: prompt param: prompt
type: str type: str
@ -27,140 +25,83 @@ class UserList(ListManager):
type: Dict type: Dict
param: sudo. boolean to determine if we handle superusers or users. If None handles both types param: sudo. boolean to determine if we handle superusers or users. If None handles both types
""" """
self.sudo = sudo self._actions = [
self.actions = [
str(_('Add a user')), str(_('Add a user')),
str(_('Change password')), str(_('Change password')),
str(_('Promote/Demote user')), str(_('Promote/Demote user')),
str(_('Delete User')) str(_('Delete User'))
] ]
super().__init__(prompt, lusers, self.actions, self.actions[0]) super().__init__(prompt, lusers, self._actions, self._actions[0])
def reformat(self, data: List) -> Dict: def reformat(self, data: List[User]) -> Dict[str, User]:
def format_element(elem :str): return {e.display(): e for e in data}
# secret gives away the length of the password
if data[elem].get('!password'):
pwd = '*' * 16
else:
pwd = ''
if data[elem].get('sudoer'):
super_user = 'Superuser'
else:
super_user = ' '
return f"{elem:16}: password {pwd:16} {super_user}"
return {format_element(e): e for e in data}
def action_list(self): def action_list(self):
if self.target: active_user = self.target if self.target else None
active_user = list(self.target.keys())[0]
else:
active_user = None
sudoer = self.target[active_user].get('sudoer', False)
if self.sudo is None:
return self.actions
if self.sudo and sudoer:
return self.actions
elif self.sudo and not sudoer:
return [self.actions[2]]
elif not self.sudo and sudoer:
return [self.actions[2]]
else:
return self.actions
def exec_action(self, data: Any): if active_user is None:
return [self._actions[0]]
else:
return self._actions[1:]
def exec_action(self, data: List[User]) -> List[User]:
if self.target: if self.target:
active_user = list(self.target.keys())[0] active_user = self.target
else: else:
active_user = None active_user = None
if self.action == self.actions[0]: # add if self.action == self._actions[0]: # add
new_user = self.add_user() new_user = self._add_user()
# no unicity check, if exists will be replaced if new_user is not None:
data.update(new_user) # in case a user with the same username as an existing user
elif self.action == self.actions[1]: # change password # was created we'll replace the existing one
data[active_user]['!password'] = get_password( data = [d for d in data if d.username != new_user.username]
prompt=str(_('Password for user "{}": ').format(active_user))) data += [new_user]
elif self.action == self.actions[2]: # promote/demote elif self.action == self._actions[1]: # change password
data[active_user]['sudoer'] = not data[active_user]['sudoer'] prompt = str(_('Password for user "{}": ').format(active_user.username))
elif self.action == self.actions[3]: # delete new_password = get_password(prompt=prompt)
del data[active_user] if new_password:
user = next(filter(lambda x: x == active_user, data), 1)
user.password = new_password
elif self.action == self._actions[2]: # promote/demote
user = next(filter(lambda x: x == active_user, data), 1)
user.sudo = False if user.sudo else True
elif self.action == self._actions[3]: # delete
data = [d for d in data if d != active_user]
return data
def _check_for_correct_username(self, username: str) -> bool: def _check_for_correct_username(self, username: str) -> bool:
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
return True return True
log("The username you entered is invalid. Try again", level=logging.WARNING, fg='red')
return False return False
def add_user(self): def _add_user(self) -> Optional[User]:
print(_('\nDefine a new user\n')) print(_('\nDefine a new user\n'))
prompt = str(_("User Name : ")) prompt = str(_('Enter username (leave blank to skip): '))
while True: while True:
userid = input(prompt).strip(' ') username = input(prompt).strip(' ')
if not userid: if not username:
return {} # end return None
if not self._check_for_correct_username(userid): if not self._check_for_correct_username(username):
pass prompt = str(_("The username you entered is invalid. Try again")) + '\n' + prompt
else: else:
break break
if self.sudo:
sudoer = True password = get_password(prompt=str(_('Password for user "{}": ').format(username)))
elif self.sudo is not None and not self.sudo:
sudoer = False choice = Menu(
else: str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(),
sudoer = False
sudo_choice = Menu(str(_('Should {} be a superuser (sudoer)?')).format(userid), Menu.yes_no(),
skip=False, skip=False,
preset_values=Menu.yes() if sudoer else Menu.no(), default_option=Menu.no()
default_option=Menu.no()).run() ).run()
sudoer = True if sudo_choice == Menu.yes() else False
password = get_password(prompt=str(_('Password for user "{}": ').format(userid))) sudo = True if choice.value == Menu.yes() else False
return User(username, password, sudo)
return {userid: {"!password": password, "sudoer": sudoer}}
def manage_users(prompt: str, sudo: bool) -> tuple[dict, dict]: def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]:
# TODO Filtering and some kind of simpler code prompt = prompt if prompt else _('Enter username (leave blank to skip): ')
lusers = {} users = UserList(prompt, defined_users).run()
if storage['arguments'].get('!superusers', {}):
lusers.update({
uid: {
'!password': storage['arguments']['!superusers'][uid].get('!password'),
'sudoer': True
}
for uid in storage['arguments'].get('!superusers', {})
})
if storage['arguments'].get('!users', {}):
lusers.update({
uid: {
'!password': storage['arguments']['!users'][uid].get('!password'),
'sudoer': False
}
for uid in storage['arguments'].get('!users', {})
})
# processing
lusers = UserList(prompt, lusers, sudo).run()
# return data
superusers = {
uid: {
'!password': lusers[uid].get('!password')
}
for uid in lusers if lusers[uid].get('sudoer', False)
}
users = {uid: {'!password': lusers[uid].get('!password')} for uid in lusers if not lusers[uid].get('sudoer', False)}
storage['arguments']['!superusers'] = superusers
storage['arguments']['!users'] = users
return superusers, users
def ask_for_superuser_account(prompt: str) -> Dict[str, Dict[str, str]]:
prompt = prompt if prompt else str(_('Define users with sudo privilege, by username: '))
superusers, dummy = manage_users(prompt, sudo=True)
return superusers
def ask_for_additional_users(prompt: str = '') -> Dict[str, Dict[str, str | None]]:
prompt = prompt if prompt else _('Any additional users to install (leave blank for no users): ')
dummy, users = manage_users(prompt, sudo=False)
return users return users

View File

@ -64,6 +64,8 @@ class ManualNetworkConfig(ListManager):
elif self.action == self._action_delete: elif self.action == self._action_delete:
del data[iface_name] del data[iface_name]
return data
def _select_iface(self, existing_ifaces: List[str]) -> Optional[Any]: def _select_iface(self, existing_ifaces: List[str]) -> Optional[Any]:
all_ifaces = list_interfaces().values() all_ifaces = list_interfaces().values()
available = set(all_ifaces) - set(existing_ifaces) available = set(all_ifaces) - set(existing_ifaces)

View File

@ -57,6 +57,8 @@ class SubvolumeList(ListManager):
data.update(self.target) data.update(self.target)
return data
class SubvolumeMenu(GeneralMenu): class SubvolumeMenu(GeneralMenu):
def __init__(self,parameters,action=None): def __init__(self,parameters,action=None):

View File

@ -163,21 +163,20 @@ Options for ``--creds``
"!root-password" : "SecretSanta2022" "!root-password" : "SecretSanta2022"
} }
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| Key | Values | Description | Required | | Key | Values | Description | Required |
| | | | | +======================+========================================================+======================================================================================+===============================================+
+======================+=====================================================+======================================================================================+===============================================+
| !encryption-password | any | Password to encrypt disk, not encrypted if password not provided | No | | !encryption-password | any | Password to encrypt disk, not encrypted if password not provided | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| !root-password | any | The root account password | No | | !root-password | any | The root account password | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| !superusers | { "<username>": { "!password": "<password>"}, ..} | List of superuser credentials, see configuration for reference | Yes[1] | | !users | { "username": "<USERNAME>" | List of regular user credentials, see configuration for reference | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ | | "!password": "<PASSWORD>", | | |
| !users | { "<username>": { "!password": "<password>"}, ..} | List of regular user credentials, see configuration for reference | No | | | "sudo": false|true} | | |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
.. note:: .. note::
[1] ``!superusers`` is optional only if ``!root-password`` was set. ``!superusers`` will be enforced otherwise and the minimum amount of superusers required will be set to 1. [1] ``!users`` is optional only if ``!root-password`` was set. ``!users`` will be enforced otherwise and the minimum amount of users with sudo privileges required will be set to 1.
Options for ``--disk_layouts`` Options for ``--disk_layouts``
------------------------------ ------------------------------

View File

@ -1,9 +1,15 @@
{ {
"!root-password": "<root password>", "!root-password": "<root password>",
"!users": { "!users": [
"username": {"!password": "<user password>"} {
"username": "<USERNAME>",
"!password": "<PASSWORD>",
"sudo": false
}, },
"!superusers": { {
"admin": {"!password": "<admin password>"} "username": "<SUDO_USERNAME>",
"!password": "<PASSWORD>",
"sudo": true
} }
]
} }

View File

@ -72,7 +72,6 @@ def ask_user_questions():
# Ask for a root password (optional, but triggers requirement for super-user if skipped) # Ask for a root password (optional, but triggers requirement for super-user if skipped)
global_menu.enable('!root-password') global_menu.enable('!root-password')
global_menu.enable('!superusers')
global_menu.enable('!users') global_menu.enable('!users')
# Ask for archinstall-specific profiles (such as desktop environments etc) # Ask for archinstall-specific profiles (such as desktop environments etc)
@ -220,13 +219,8 @@ def perform_installation(mountpoint):
if archinstall.arguments.get('profile', None): if archinstall.arguments.get('profile', None):
installation.install_profile(archinstall.arguments.get('profile', None)) installation.install_profile(archinstall.arguments.get('profile', None))
if archinstall.arguments.get('!users',{}): if users := archinstall.arguments.get('!users', None):
for user, user_info in archinstall.arguments.get('!users', {}).items(): installation.create_users(users)
installation.user_create(user, user_info["!password"], sudo=False)
if archinstall.arguments.get('!superusers',{}):
for superuser, user_info in archinstall.arguments.get('!superusers', {}).items():
installation.user_create(superuser, user_info["!password"], sudo=True)
if timezone := archinstall.arguments.get('timezone', None): if timezone := archinstall.arguments.get('timezone', None):
installation.set_timezone(timezone) installation.set_timezone(timezone)

View File

@ -1,6 +1,8 @@
import archinstall import archinstall
# Select a harddrive and a disk password # Select a harddrive and a disk password
from archinstall import User
archinstall.log("Minimal only supports:") archinstall.log("Minimal only supports:")
archinstall.log(" * Being installed to a single disk") archinstall.log(" * Being installed to a single disk")
@ -28,8 +30,8 @@ def install_on(mountpoint):
installation.add_additional_packages(['nano', 'wget', 'git']) installation.add_additional_packages(['nano', 'wget', 'git'])
installation.install_profile('minimal') installation.install_profile('minimal')
installation.user_create('devel', 'devel') user = User('devel', 'devel', False)
installation.user_set_pw('root', 'airoot') installation.create_users(user)
# Once this is done, we output some useful information to the user # Once this is done, we output some useful information to the user
# And the installation is complete. # And the installation is complete.

View File

@ -219,7 +219,7 @@ class MyMenu(archinstall.GlobalMenu):
if self._execution_mode in ('full','lineal'): if self._execution_mode in ('full','lineal'):
options_list = ['keyboard-layout', 'mirror-region', 'harddrives', 'disk_layouts', options_list = ['keyboard-layout', 'mirror-region', 'harddrives', 'disk_layouts',
'!encryption-password','swap', 'bootloader', 'hostname', '!root-password', '!encryption-password','swap', 'bootloader', 'hostname', '!root-password',
'!superusers', '!users', 'profile', 'audio', 'kernels', 'packages','additional-repositories','nic', '!users', 'profile', 'audio', 'kernels', 'packages','additional-repositories','nic',
'timezone', 'ntp'] 'timezone', 'ntp']
if archinstall.arguments.get('advanced',False): if archinstall.arguments.get('advanced',False):
options_list.extend(['sys-language','sys-encoding']) options_list.extend(['sys-language','sys-encoding'])
@ -229,7 +229,7 @@ class MyMenu(archinstall.GlobalMenu):
mandatory_list = ['harddrives'] mandatory_list = ['harddrives']
elif self._execution_mode == 'only_os': elif self._execution_mode == 'only_os':
options_list = ['keyboard-layout', 'mirror-region','bootloader', 'hostname', options_list = ['keyboard-layout', 'mirror-region','bootloader', 'hostname',
'!root-password', '!superusers', '!users', 'profile', 'audio', 'kernels', '!root-password', '!users', 'profile', 'audio', 'kernels',
'packages', 'additional-repositories', 'nic', 'timezone', 'ntp'] 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp']
mandatory_list = ['hostname'] mandatory_list = ['hostname']
if archinstall.arguments.get('advanced',False): if archinstall.arguments.get('advanced',False):
@ -262,8 +262,12 @@ class MyMenu(archinstall.GlobalMenu):
def check(s): def check(s):
return self.option(s).has_selection() return self.option(s).has_selection()
def has_superuser() -> bool:
users = self._menu_options['!users'].current_selection
return any([u.sudo for u in users])
_, missing = self.mandatory_overview() _, missing = self.mandatory_overview()
if mode in ('full','only_os') and (not check('!root-password') and not check('!superusers')): if mode in ('full','only_os') and (not check('!root-password') and not has_superuser()):
missing += 1 missing += 1
if mode in ('full', 'only_hd') and check('harddrives'): if mode in ('full', 'only_hd') and check('harddrives'):
if not self.option('harddrives').is_empty() and not check('disk_layouts'): if not self.option('harddrives').is_empty() and not check('disk_layouts'):
@ -420,13 +424,8 @@ def os_setup(installation):
if archinstall.arguments.get('profile', None): if archinstall.arguments.get('profile', None):
installation.install_profile(archinstall.arguments.get('profile', None)) installation.install_profile(archinstall.arguments.get('profile', None))
if archinstall.arguments.get('!users',{}): if users := archinstall.arguments.get('!users', None):
for user, user_info in archinstall.arguments.get('!users', {}).items(): installation.create_users(users)
installation.user_create(user, user_info["!password"], sudo=False)
if archinstall.arguments.get('!superusers',{}):
for superuser, user_info in archinstall.arguments.get('!superusers', {}).items():
installation.user_create(superuser, user_info["!password"], sudo=True)
if timezone := archinstall.arguments.get('timezone', None): if timezone := archinstall.arguments.get('timezone', None):
installation.set_timezone(timezone) installation.set_timezone(timezone)

View File

@ -40,7 +40,8 @@ with archinstall.Filesystem(harddrive) as fs:
installation.add_additional_packages(__packages__) installation.add_additional_packages(__packages__)
installation.install_profile('awesome') installation.install_profile('awesome')
installation.user_create('devel', 'devel') user = User('devel', 'devel', False)
installation.create_users(user)
installation.user_set_pw('root', 'toor') installation.user_set_pw('root', 'toor')
print(f'Submitting {archinstall.__version__}: success') print(f'Submitting {archinstall.__version__}: success')

View File

@ -167,7 +167,15 @@
}, },
{ {
"required": [ "required": [
"!superusers" "!users": {
"description": "User account",
"type": "object",
"properties": {
"username": "string",
"!password": "string",
"sudo": "boolean"
}
}
] ]
} }
] ]