diff options
author | Christian Breunig <christian@breunig.cc> | 2023-12-30 23:25:20 +0100 |
---|---|---|
committer | Christian Breunig <christian@breunig.cc> | 2024-01-01 09:25:32 +0100 |
commit | c9eaafd9f808aba8d29be73054e11d37577e539a (patch) | |
tree | aeccfda0a305cf6aca41630900e75bd32961a911 /src/conf_mode/system-login.py | |
parent | 2078253176046ea4d07e69caeb7932ea439b5614 (diff) | |
download | vyos-1x-c9eaafd9f808aba8d29be73054e11d37577e539a.tar.gz vyos-1x-c9eaafd9f808aba8d29be73054e11d37577e539a.zip |
T5474: establish common file name pattern for XML conf mode commands
We will use _ as CLI level divider. The XML definition filename and also
the Python helper should match the CLI node.
Example:
set interfaces ethernet -> interfaces_ethernet.xml.in
set interfaces bond -> interfaces_bond.xml.in
set service dhcp-server -> service_dhcp-server-xml.in
(cherry picked from commit 4ef110fd2c501b718344c72d495ad7e16d2bd465)
Diffstat (limited to 'src/conf_mode/system-login.py')
-rwxr-xr-x | src/conf_mode/system-login.py | 423 |
1 files changed, 0 insertions, 423 deletions
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py deleted file mode 100755 index 95021c8fd..000000000 --- a/src/conf_mode/system-login.py +++ /dev/null @@ -1,423 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020-2023 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os - -from passlib.hosts import linux_context -from psutil import users -from pwd import getpwall -from pwd import getpwnam -from sys import exit -from time import sleep - -from vyos.config import Config -from vyos.configverify import verify_vrf -from vyos.defaults import directories -from vyos.template import render -from vyos.template import is_ipv4 -from vyos.utils.dict import dict_search -from vyos.utils.file import chown -from vyos.utils.process import cmd -from vyos.utils.process import call -from vyos.utils.process import rc_cmd -from vyos.utils.process import run -from vyos.utils.process import DEVNULL -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -autologout_file = "/etc/profile.d/autologout.sh" -limits_file = "/etc/security/limits.d/10-vyos.conf" -radius_config_file = "/etc/pam_radius_auth.conf" -tacacs_pam_config_file = "/etc/tacplus_servers" -tacacs_nss_config_file = "/etc/tacplus_nss.conf" -nss_config_file = "/etc/nsswitch.conf" - -# Minimum UID used when adding system users -MIN_USER_UID: int = 1000 -# Maximim UID used when adding system users -MAX_USER_UID: int = 59999 -# LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec -MAX_RADIUS_TIMEOUT: int = 50 -# MAX_RADIUS_TIMEOUT divided by 2 sec (minimum recomended timeout) -MAX_RADIUS_COUNT: int = 8 -# Maximum number of supported TACACS servers -MAX_TACACS_COUNT: int = 8 - -# List of local user accounts that must be preserved -SYSTEM_USER_SKIP_LIST: list = ['radius_user', 'radius_priv_user', 'tacacs0', 'tacacs1', - 'tacacs2', 'tacacs3', 'tacacs4', 'tacacs5', 'tacacs6', - 'tacacs7', 'tacacs8', 'tacacs9', 'tacacs10',' tacacs11', - 'tacacs12', 'tacacs13', 'tacacs14', 'tacacs15'] - -def get_local_users(): - """Return list of dynamically allocated users (see Debian Policy Manual)""" - local_users = [] - for s_user in getpwall(): - if getpwnam(s_user.pw_name).pw_uid < MIN_USER_UID: - continue - if getpwnam(s_user.pw_name).pw_uid > MAX_USER_UID: - continue - if s_user.pw_name in SYSTEM_USER_SKIP_LIST: - continue - local_users.append(s_user.pw_name) - - return local_users - -def get_shadow_password(username): - with open('/etc/shadow') as f: - for user in f.readlines(): - items = user.split(":") - if username == items[0]: - return items[1] - return None - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['system', 'login'] - login = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) - - # users no longer existing in the running configuration need to be deleted - local_users = get_local_users() - cli_users = [] - if 'user' in login: - cli_users = list(login['user']) - - # prune TACACS global defaults if not set by user - if login.from_defaults(['tacacs']): - del login['tacacs'] - # same for RADIUS - if login.from_defaults(['radius']): - del login['radius'] - - # create a list of all users, cli and users - all_users = list(set(local_users + cli_users)) - # We will remove any normal users that dos not exist in the current - # configuration. This can happen if user is added but configuration was not - # saved and system is rebooted. - rm_users = [tmp for tmp in all_users if tmp not in cli_users] - if rm_users: login.update({'rm_users' : rm_users}) - - return login - -def verify(login): - if 'rm_users' in login: - # This check is required as the script is also executed from vyos-router - # init script and there is no SUDO_USER environment variable available - # during system boot. - if 'SUDO_USER' in os.environ: - cur_user = os.environ['SUDO_USER'] - if cur_user in login['rm_users']: - raise ConfigError(f'Attempting to delete current user: {cur_user}') - - if 'user' in login: - system_users = getpwall() - for user, user_config in login['user'].items(): - # Linux system users range up until UID 1000, we can not create a - # VyOS CLI user which already exists as system user - for s_user in system_users: - if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID: - raise ConfigError(f'User "{user}" can not be created, conflict with local system account!') - - for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items(): - if 'type' not in pubkey_options: - raise ConfigError(f'Missing type for public-key "{pubkey}"!') - if 'key' not in pubkey_options: - raise ConfigError(f'Missing key for public-key "{pubkey}"!') - - if {'radius', 'tacacs'} <= set(login): - raise ConfigError('Using both RADIUS and TACACS at the same time is not supported!') - - # At lease one RADIUS server must not be disabled - if 'radius' in login: - if 'server' not in login['radius']: - raise ConfigError('No RADIUS server defined!') - sum_timeout: int = 0 - radius_servers_count: int = 0 - fail = True - for server, server_config in dict_search('radius.server', login).items(): - if 'key' not in server_config: - raise ConfigError(f'RADIUS server "{server}" requires key!') - if 'disable' not in server_config: - sum_timeout += int(server_config['timeout']) - radius_servers_count += 1 - fail = False - - if fail: - raise ConfigError('All RADIUS servers are disabled') - - if radius_servers_count > MAX_RADIUS_COUNT: - raise ConfigError(f'Number of RADIUS servers exceeded maximum of {MAX_RADIUS_COUNT}!') - - if sum_timeout > MAX_RADIUS_TIMEOUT: - raise ConfigError('Sum of RADIUS servers timeouts ' - 'has to be less or eq 50 sec') - - verify_vrf(login['radius']) - - if 'source_address' in login['radius']: - ipv4_count = 0 - ipv6_count = 0 - for address in login['radius']['source_address']: - if is_ipv4(address): ipv4_count += 1 - else: ipv6_count += 1 - - if ipv4_count > 1: - raise ConfigError('Only one IPv4 source-address can be set!') - if ipv6_count > 1: - raise ConfigError('Only one IPv6 source-address can be set!') - - if 'tacacs' in login: - tacacs_servers_count: int = 0 - fail = True - for server, server_config in dict_search('tacacs.server', login).items(): - if 'key' not in server_config: - raise ConfigError(f'TACACS server "{server}" requires key!') - if 'disable' not in server_config: - tacacs_servers_count += 1 - fail = False - - if fail: - raise ConfigError('All RADIUS servers are disabled') - - if tacacs_servers_count > MAX_TACACS_COUNT: - raise ConfigError(f'Number of TACACS servers exceeded maximum of {MAX_TACACS_COUNT}!') - - verify_vrf(login['tacacs']) - - if 'max_login_session' in login and 'timeout' not in login: - raise ConfigError('"login timeout" must be configured!') - - return None - - -def generate(login): - # calculate users encrypted password - if 'user' in login: - for user, user_config in login['user'].items(): - tmp = dict_search('authentication.plaintext_password', user_config) - if tmp: - encrypted_password = linux_context.hash(tmp) - login['user'][user]['authentication']['encrypted_password'] = encrypted_password - del login['user'][user]['authentication']['plaintext_password'] - - # remove old plaintext password and set new encrypted password - env = os.environ.copy() - env['vyos_libexec_dir'] = directories['base'] - - # Set default commands for re-adding user with encrypted password - del_user_plain = f"system login user {user} authentication plaintext-password" - add_user_encrypt = f"system login user {user} authentication encrypted-password '{encrypted_password}'" - - lvl = env['VYATTA_EDIT_LEVEL'] - # We're in config edit level, for example "edit system login" - # Change default commands for re-adding user with encrypted password - if lvl != '/': - # Replace '/system/login' to 'system login' - lvl = lvl.strip('/').split('/') - # Convert command str to list - del_user_plain = del_user_plain.split() - # New command exclude level, for example "edit system login" - del_user_plain = del_user_plain[len(lvl):] - # Convert string to list - del_user_plain = " ".join(del_user_plain) - - add_user_encrypt = add_user_encrypt.split() - add_user_encrypt = add_user_encrypt[len(lvl):] - add_user_encrypt = " ".join(add_user_encrypt) - - ret, out = rc_cmd(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env) - if ret: raise ConfigError(out) - ret, out = rc_cmd(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env) - if ret: raise ConfigError(out) - else: - try: - if get_shadow_password(user) == dict_search('authentication.encrypted_password', user_config): - # If the current encrypted bassword matches the encrypted password - # from the config - do not update it. This will remove the encrypted - # value from the system logs. - # - # The encrypted password will be set only once during the first boot - # after an image upgrade. - del login['user'][user]['authentication']['encrypted_password'] - except: - pass - - ### RADIUS based user authentication - if 'radius' in login: - render(radius_config_file, 'login/pam_radius_auth.conf.j2', login, - permission=0o600, user='root', group='root') - else: - if os.path.isfile(radius_config_file): - os.unlink(radius_config_file) - - ### TACACS+ based user authentication - if 'tacacs' in login: - render(tacacs_pam_config_file, 'login/tacplus_servers.j2', login, - permission=0o644, user='root', group='root') - render(tacacs_nss_config_file, 'login/tacplus_nss.conf.j2', login, - permission=0o644, user='root', group='root') - else: - if os.path.isfile(tacacs_pam_config_file): - os.unlink(tacacs_pam_config_file) - if os.path.isfile(tacacs_nss_config_file): - os.unlink(tacacs_nss_config_file) - - - - # NSS must always be present on the system - render(nss_config_file, 'login/nsswitch.conf.j2', login, - permission=0o644, user='root', group='root') - - # /etc/security/limits.d/10-vyos.conf - if 'max_login_session' in login: - render(limits_file, 'login/limits.j2', login, - permission=0o644, user='root', group='root') - else: - if os.path.isfile(limits_file): - os.unlink(limits_file) - - if 'timeout' in login: - render(autologout_file, 'login/autologout.j2', login, - permission=0o755, user='root', group='root') - else: - if os.path.isfile(autologout_file): - os.unlink(autologout_file) - - return None - - -def apply(login): - enable_otp = False - if 'user' in login: - for user, user_config in login['user'].items(): - # make new user using vyatta shell and make home directory (-m), - # default group of 100 (users) - command = 'useradd --create-home --no-user-group ' - # check if user already exists: - if user in get_local_users(): - # update existing account - command = 'usermod' - - # all accounts use /bin/vbash - command += ' --shell /bin/vbash' - # we need to use '' quotes when passing formatted data to the shell - # else it will not work as some data parts are lost in translation - tmp = dict_search('authentication.encrypted_password', user_config) - if tmp: command += f" --password '{tmp}'" - - tmp = dict_search('full_name', user_config) - if tmp: command += f" --comment '{tmp}'" - - tmp = dict_search('home_directory', user_config) - if tmp: command += f" --home '{tmp}'" - else: command += f" --home '/home/{user}'" - - command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}' - try: - cmd(command) - # we should not rely on the value stored in - # user_config['home_directory'], as a crazy user will choose - # username root or any other system user which will fail. - # - # XXX: Should we deny using root at all? - home_dir = getpwnam(user).pw_dir - # T5875: ensure UID is properly set on home directory if user is re-added - if os.path.exists(home_dir): - chown(home_dir, user=user, recursive=True) - - render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.j2', - user_config, permission=0o600, - formater=lambda _: _.replace(""", '"'), - user=user, group='users') - - except Exception as e: - raise ConfigError(f'Adding user "{user}" raised exception: "{e}"') - - # Generate 2FA/MFA One-Time-Pad configuration - if dict_search('authentication.otp.key', user_config): - enable_otp = True - render(f'{home_dir}/.google_authenticator', 'login/pam_otp_ga.conf.j2', - user_config, permission=0o400, user=user, group='users') - else: - # delete configuration as it's not enabled for the user - if os.path.exists(f'{home_dir}/.google_authenticator'): - os.remove(f'{home_dir}/.google_authenticator') - - if 'rm_users' in login: - for user in login['rm_users']: - try: - # Disable user to prevent re-login - call(f'usermod -s /sbin/nologin {user}') - - # Logout user if he is still logged in - if user in list(set([tmp[0] for tmp in users()])): - print(f'{user} is logged in, forcing logout!') - # re-run command until user is logged out - while run(f'pkill -HUP -u {user}'): - sleep(0.250) - - # Remove user account but leave home directory in place. Re-run - # command until user is removed - userdel might return 8 as - # SSH sessions are not all yet properly cleaned away, thus we - # simply re-run the command until the account wen't away - while run(f'userdel {user}', stderr=DEVNULL): - sleep(0.250) - - except Exception as e: - raise ConfigError(f'Deleting user "{user}" raised exception: {e}') - - # Enable/disable RADIUS in PAM configuration - cmd('pam-auth-update --disable radius-mandatory radius-optional') - if 'radius' in login: - if login['radius'].get('security_mode', '') == 'mandatory': - pam_profile = 'radius-mandatory' - else: - pam_profile = 'radius-optional' - cmd(f'pam-auth-update --enable {pam_profile}') - - # Enable/disable TACACS+ in PAM configuration - cmd('pam-auth-update --disable tacplus-mandatory tacplus-optional') - if 'tacacs' in login: - if login['tacacs'].get('security_mode', '') == 'mandatory': - pam_profile = 'tacplus-mandatory' - else: - pam_profile = 'tacplus-optional' - cmd(f'pam-auth-update --enable {pam_profile}') - - # Enable/disable Google authenticator - cmd('pam-auth-update --disable mfa-google-authenticator') - if enable_otp: - cmd(f'pam-auth-update --enable mfa-google-authenticator') - - return None - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) |