diff options
author | Christian Poessinger <christian@poessinger.com> | 2021-01-08 18:18:42 +0100 |
---|---|---|
committer | Christian Poessinger <christian@poessinger.com> | 2021-01-08 18:18:42 +0100 |
commit | 43a9441cb80a14fff791bbd89e88a3c2ac99e3ab (patch) | |
tree | 2bf9c09fab5c95efa73fc2f7601890466fa9b99f /src | |
parent | 13a58d38b3dc8065a8ba71904e143e3d69aab638 (diff) | |
parent | 23f55c4bcbe5475ed98d57cf54b645ef0c2cc1a8 (diff) | |
download | vyos-1x-43a9441cb80a14fff791bbd89e88a3c2ac99e3ab.tar.gz vyos-1x-43a9441cb80a14fff791bbd89e88a3c2ac99e3ab.zip |
Merge branch 'current' of github.com:vyos/vyos-1x into equuleus
* 'current' of github.com:vyos/vyos-1x: (30 commits)
smoketest: dummy: fix indent
smoketest: bridge: bond: enable ip subsystem tests
smoketest: interfaces: dhcpv6pd final fix
smoketest: ethernet: fix link-speed loop test
Debian: add build-dependency on python3-jinja2
smoketest: ethernet: verify() speed/duplex must both be auto or discrete
smoketest: interfaces: report skipped tests
smoketest: ethernet: bugfixes for dhcpc6 and unknown interfaces
Debian: add python3-psutil build dependency
smoketest: ethernet: check for error on non existing interface
vyos.configverify: provide generic helper to check for interface existence
smoketest: interfaces: fix dhcpv6 pd testcase when using multiple interfaces
login: radius: T3192: migrate to get_config_dict()
ssh: T2635: harden Jinja2 template and daemon startup
ssh: T2635: change sshd_config path to /run/sshd
login: radius: T3192: support IPv6 server(s) and source-address
xml: include: provide generic include for disable node
xml: radius: T3192: split individual nodes to discrete includes
bgp: T2174: verify() existence of route-map and prefix-list
smoketest: interfaces: test dhcpv6 pd sla-id auto increment
...
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/interfaces-ethernet.py | 19 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-tunnel.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/protocols_bgp.py | 83 | ||||
-rwxr-xr-x | src/conf_mode/protocols_isis.py | 33 | ||||
-rwxr-xr-x | src/conf_mode/ssh.py | 7 | ||||
-rwxr-xr-x | src/conf_mode/system-login.py | 482 |
6 files changed, 294 insertions, 338 deletions
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index bc102826f..e7f0cd6a5 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -23,13 +23,13 @@ from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_address from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_eapol from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_mirror from vyos.configverify import verify_mtu from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf -from vyos.configverify import verify_eapol -from vyos.configverify import verify_mirror from vyos.ifconfig import EthernetIf from vyos.template import render from vyos.util import call @@ -59,15 +59,13 @@ def verify(ethernet): if 'deleted' in ethernet: return None - verify_interface_exists(ethernet) - - if ethernet.get('speed', None) == 'auto': - if ethernet.get('duplex', None) != 'auto': - raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too') + ifname = ethernet['ifname'] + verify_interface_exists(ifname) - if ethernet.get('duplex', None) == 'auto': - if ethernet.get('speed', None) != 'auto': - raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too') + # No need to check speed and duplex keys as both have default values. + if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or + (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')): + raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured') verify_mtu(ethernet) verify_mtu_ipv6(ethernet) @@ -77,7 +75,6 @@ def verify(ethernet): verify_eapol(ethernet) verify_mirror(ethernet) - ifname = ethernet['ifname'] # verify offloading capabilities if 'offload' in ethernet and 'rps' in ethernet['offload']: if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'): diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 1a7e9a96d..ffeb57784 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2021 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 @@ -24,10 +24,11 @@ from vyos.configdict import dict_merge from vyos.configdict import get_interface_dict from vyos.configdict import node_changed from vyos.configdict import leaf_node_changed -from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_interface_exists from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_vrf from vyos.ifconfig import Interface from vyos.ifconfig import GREIf from vyos.ifconfig import GRETapIf @@ -122,6 +123,9 @@ def verify(tunnel): if 'local_ip' in tunnel and is_ipv6(tunnel['local_ip']): raise ConfigError('Can not use local IPv6 address is for mGRE tunnels') + if 'source_interface' in tunnel: + verify_interface_exists(tunnel['source_interface']) + def generate(tunnel): return None diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index a3f32fd2d..678be5066 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -17,10 +17,11 @@ from sys import exit from vyos.config import Config -from vyos.util import call -from vyos.util import dict_search +from vyos.configdict import dict_merge from vyos.template import render from vyos.template import render_to_string +from vyos.util import call +from vyos.util import dict_search from vyos import ConfigError from vyos import frr from vyos import airbag @@ -42,6 +43,16 @@ def get_config(): if not conf.exists(base + ['route-map']): call('vtysh -c \"conf t\" -c \"no ip protocol bgp\" ') + # We also need some additional information from the config, + # prefix-lists and route-maps for instance. + base = ['policy'] + tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # As we only support one ASN (later checked in begin of verify()) we add the + # new information only to the first AS number + asn = next(iter(bgp)) + # Merge policy dict into bgp dict + bgp[asn] = dict_merge(tmp, bgp[asn]) + return bgp def verify(bgp): @@ -50,32 +61,64 @@ def verify(bgp): # Check if declared more than one ASN if len(bgp) > 1: - raise ConfigError('Only one BGP AS can be defined!') + raise ConfigError('Only one BGP AS number can be defined!') for asn, asn_config in bgp.items(): + import pprint + pprint.pprint(asn_config) + # Common verification for both peer-group and neighbor statements - for neigh in ['neighbor', 'peer_group']: + for neighbor in ['neighbor', 'peer_group']: # bail out early if there is no neighbor or peer-group statement # this also saves one indention level - if neigh not in asn_config: + if neighbor not in asn_config: + print(f'no {neighbor} found in config') continue - #for neighbor, config in asn_config[neigh].items(): - ''' - # These checks need to be modified. Because peer-group can be declared without 'remote-as'. - # When 'remote-as' configured for specific neighbor in peer-group. For example - # - - set protocols nbgp 65001 neighbor 100.64.0.2 peer-group 'FOO' - set protocols nbgp 65001 neighbor 100.64.0.2 remote-as '65002' - set protocols nbgp 65001 peer-group FOO - - ''' - #if 'remote_as' not in config and 'peer_group' not in config: - # raise ConfigError(f'BGP remote-as must be specified for "{neighbor}"!') + for peer, peer_config in asn_config[neighbor].items(): + # Only regular "neighbor" statement can have a peer-group set + # Check if the configure peer-group exists + if 'peer_group' in peer_config: + peer_group = peer_config['peer_group'] + if peer_group not in asn_config['peer_group']: + raise ConfigError(f'Specified peer-group "{peer_group}" for '\ + f'neighbor "{neighbor}" does not exist!') + + # Some checks can/must only be done on a neighbor and nor a peer-group + if neighbor == 'neighbor': + # remote-as must be either set explicitly for the neighbor + # or for the entire peer-group + if 'remote_as' not in peer_config: + if 'peer_group' not in peer_config or 'remote_as' not in asn_config['peer_group'][peer_config['peer_group']]: + raise ConfigError('Remote AS must be set for neighbor or peer-group!') + + for afi in ['ipv4_unicast', 'ipv6_unicast']: + # Bail out early if address family is not configured + if 'address_family' not in peer_config or afi not in peer_config['address_family']: + continue + + afi_config = peer_config['address_family'][afi] + # Validate if configured Prefix list exists + if 'prefix_list' in afi_config: + for tmp in ['import', 'export']: + if tmp in afi_config['prefix_list']: + if afi == 'ipv4_unicast': + prefix_list = afi_config['prefix_list'][tmp] + if 'prefix_list' not in asn_config or prefix_list not in asn_config['prefix_list']: + raise ConfigError(f'prefix-list "{prefix_list}" used for "{tmp}" does not exist!') + if afi == 'ipv6_unicast': + prefix_list = afi_config['prefix_list6'][tmp] + if 'prefix_list6' not in asn_config or prefix_list not in asn_config['prefix_list6']: + raise ConfigError(f'prefix-list "{prefix_list}" used for "{tmp}" does not exist!') + + + if 'route_map' in afi_config: + for tmp in ['import', 'export']: + if tmp in afi_config['route_map']: + route_map = afi_config['route_map'][tmp] + if 'route_map' not in asn_config or route_map not in asn_config['route_map']: + raise ConfigError(f'route-map "{route_map}" used for "{tmp}" does not exist!') - #if 'remote_as' in config and 'peer_group' in config: - # raise ConfigError(f'BGP peer-group member "{neighbor}" cannot override remote-as of peer-group!') return None diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 97ab79583..b7afad473 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -22,6 +22,7 @@ from vyos.config import Config from vyos.configdict import node_changed from vyos import ConfigError from vyos.util import call +from vyos.util import dict_search from vyos.template import render from vyos.template import render_to_string from vyos import frr @@ -48,7 +49,7 @@ def verify(isis): # If more then one isis process is defined (Frr only supports one) # http://docs.frrouting.org/en/latest/isisd.html#isis-router if len(isis) > 1: - raise ConfigError('Only one isis process can be definded') + raise ConfigError('Only one isis process can be defined') # If network entity title (net) not defined if 'net' not in isis_config: @@ -63,7 +64,7 @@ def verify(isis): if {'md5', 'plaintext_password'} <= set(isis_config['encryption']): raise ConfigError('Can not use both md5 and plaintext-password for ISIS area-password!') - # If one param from deley set, but not set others + # If one param from delay set, but not set others if 'spf_delay_ietf' in isis_config: required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn'] exist_timers = [] @@ -85,6 +86,34 @@ def verify(isis): if proc_level and proc_level != 'level_1_2' and proc_level != redistribute_level: raise ConfigError('\"protocols isis {0} redistribute ipv4 {2} {3}\" cannot be used with \"protocols isis {0} level {1}\"'.format(process, proc_level, proto, redistribute_level)) + # Segment routing checks + if dict_search('segment_routing', isis_config): + if dict_search('segment_routing.global_block', isis_config): + high_label_value = dict_search('segment_routing.global_block.high_label_value', isis_config) + low_label_value = dict_search('segment_routing.global_block.low_label_value', isis_config) + # If segment routing global block high value is blank, throw error + if low_label_value and not high_label_value: + raise ConfigError('Segment routing global block high value must not be left blank') + # If segment routing global block low value is blank, throw error + if high_label_value and not low_label_value: + raise ConfigError('Segment routing global block low value must not be left blank') + # If segment routing global block low value is higher than the high value, throw error + if int(low_label_value) > int(high_label_value): + raise ConfigError('Segment routing global block low value must be lower than high value') + + if dict_search('segment_routing.local_block', isis_config): + high_label_value = dict_search('segment_routing.local_block.high_label_value', isis_config) + low_label_value = dict_search('segment_routing.local_block.low_label_value', isis_config) + # If segment routing local block high value is blank, throw error + if low_label_value and not high_label_value: + raise ConfigError('Segment routing local block high value must not be left blank') + # If segment routing local block low value is blank, throw error + if high_label_value and not low_label_value: + raise ConfigError('Segment routing local block low value must not be left blank') + # If segment routing local block low value is higher than the high value, throw error + if int(low_label_value) > int(high_label_value): + raise ConfigError('Segment routing local block low value must be lower than high value') + return None def generate(isis): diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 8f99053d2..28e606663 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -28,7 +28,7 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/run/ssh/sshd_config' +config_file = r'/run/sshd/sshd_config' systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' def get_config(config=None): @@ -68,6 +68,8 @@ def generate(ssh): render(config_file, 'ssh/sshd_config.tmpl', ssh) render(systemd_override, 'ssh/override.conf.tmpl', ssh) + # Reload systemd manager configuration + call('systemctl daemon-reload') return None @@ -76,9 +78,6 @@ def apply(ssh): # SSH access is removed in the commit call('systemctl stop ssh.service') - # Reload systemd manager configuration - call('systemctl daemon-reload') - if ssh: call('systemctl restart ssh.service') diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 39bad717d..82accd404 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -16,33 +16,30 @@ import os -from crypt import crypt, METHOD_SHA512 -from netifaces import interfaces +from crypt import crypt +from crypt import METHOD_SHA512 from psutil import users -from pwd import getpwall, getpwnam +from pwd import getpwall +from pwd import getpwnam from spwd import getspnam from sys import exit from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configverify import verify_vrf from vyos.template import render -from vyos.util import cmd, call, DEVNULL, chmod_600, chmod_755 +from vyos.template import is_ipv4 +from vyos.util import cmd +from vyos.util import call +from vyos.util import DEVNULL +from vyos.util import dict_search +from vyos.xml import defaults from vyos import ConfigError - from vyos import airbag airbag.enable() radius_config_file = "/etc/pam_radius_auth.conf" -default_config_data = { - 'deleted': False, - 'add_users': [], - 'del_users': [], - 'radius_server': [], - 'radius_source_address': '', - 'radius_vrf': '' -} - - def get_local_users(): """Return list of dynamically allocated users (see Debian Policy Manual)""" local_users = [] @@ -57,211 +54,132 @@ def get_local_users(): def get_config(config=None): - login = default_config_data if config: conf = config else: conf = Config() - base_level = ['system', 'login'] - - # We do not need to check if the nodes exist or not and bail out early - # ... this would interrupt the following logic on determine which users - # should be deleted and which users should stay. - # - # All fine so far! - - # Read in all local users and store to list - for username in conf.list_nodes(base_level + ['user']): - user = { - 'name': username, - 'password_plaintext': '', - 'password_encrypted': '!', - 'public_keys': [], - 'full_name': '', - 'home_dir': '/home/' + username, - } - conf.set_level(base_level + ['user', username]) - - # Plaintext password - if conf.exists(['authentication', 'plaintext-password']): - user['password_plaintext'] = conf.return_value( - ['authentication', 'plaintext-password']) - - # Encrypted password - if conf.exists(['authentication', 'encrypted-password']): - user['password_encrypted'] = conf.return_value( - ['authentication', 'encrypted-password']) - - # User real name - if conf.exists(['full-name']): - user['full_name'] = conf.return_value(['full-name']) - - # User home-directory - if conf.exists(['home-directory']): - user['home_dir'] = conf.return_value(['home-directory']) - - # Read in public keys - for id in conf.list_nodes(['authentication', 'public-keys']): - key = { - 'name': id, - 'key': '', - 'options': '', - 'type': '' - } - conf.set_level(base_level + ['user', username, 'authentication', - 'public-keys', id]) - - # Public Key portion - if conf.exists(['key']): - key['key'] = conf.return_value(['key']) - - # Options for individual public key - if conf.exists(['options']): - key['options'] = conf.return_value(['options']) - - # Type of public key - if conf.exists(['type']): - key['type'] = conf.return_value(['type']) - - # Append individual public key to list of user keys - user['public_keys'].append(key) - - login['add_users'].append(user) - - # - # RADIUS configuration - # - conf.set_level(base_level + ['radius']) - - if conf.exists(['source-address']): - login['radius_source_address'] = conf.return_value(['source-address']) - - # retrieve VRF instance - if conf.exists(['vrf']): - login['radius_vrf'] = conf.return_value(['vrf']) - - # Read in all RADIUS servers and store to list - for server in conf.list_nodes(['server']): - server_cfg = { - 'address': server, - 'disabled': False, - 'key': '', - 'port': '1812', - 'timeout': '2', - 'priority': 255 - } - conf.set_level(base_level + ['radius', 'server', server]) - - # Check if RADIUS server was temporary disabled - if conf.exists(['disable']): - server_cfg['disabled'] = True - - # RADIUS shared secret - if conf.exists(['key']): - server_cfg['key'] = conf.return_value(['key']) - - # RADIUS authentication port - if conf.exists(['port']): - server_cfg['port'] = conf.return_value(['port']) - - # RADIUS session timeout - if conf.exists(['timeout']): - server_cfg['timeout'] = conf.return_value(['timeout']) - - # Check if RADIUS server has priority - if conf.exists(['priority']): - server_cfg['priority'] = int(conf.return_value(['priority'])) - - # Append individual RADIUS server configuration to global server list - login['radius_server'].append(server_cfg) + base = ['system', 'login'] + login = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) # users no longer existing in the running configuration need to be deleted local_users = get_local_users() - cli_users = [tmp['name'] for tmp in login['add_users']] - # create a list of all users, cli and users - all_users = list(set(local_users+cli_users)) + cli_users = [] + if 'user' in login: + cli_users = list(login['user']) + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + default_values = defaults(base + ['user']) + for user in login['user']: + login['user'][user] = dict_merge(default_values, login['user'][user]) + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + default_values = defaults(base + ['radius', 'server']) + for server in dict_search('radius.server', login) or []: + login['radius']['server'][server] = dict_merge(default_values, + login['radius']['server'][server]) + + # XXX: for a yet unknown reason when we only have one source-address + # get_config_dict() will show a string over a string + if 'radius' in login and 'source_address' in login['radius']: + print(type(login['radius']['source_address'])) + if isinstance(login['radius']['source_address'], str): + login['radius']['source_address'] = [login['radius']['source_address']] - # 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. - login['del_users'] = [tmp for tmp in all_users if tmp not in cli_users] + # 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): - cur_user = os.environ['SUDO_USER'] - if cur_user in login['del_users']: - raise ConfigError( - 'Attempting to delete current user: {}'.format(cur_user)) - - for user in login['add_users']: - for key in user['public_keys']: - if not key['type']: - raise ConfigError( - 'SSH public key type missing for "{name}"!'.format(**key)) - - if not key['key']: - raise ConfigError( - 'SSH public key for id "{name}" missing!'.format(**key)) + if 'rm_users' in login: + 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: + for user, user_config in login['user'].items(): + 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}"!') # At lease one RADIUS server must not be disabled - if len(login['radius_server']) > 0: + if 'radius' in login: + if 'server' not in login['radius']: + raise ConfigError('No RADIUS server defined!') + fail = True - for server in login['radius_server']: - if not server['disabled']: + 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 'disabled' not in server_config: fail = False + continue if fail: - raise ConfigError('At least one RADIUS server must be active.') + raise ConfigError('All RADIUS servers are disabled') + + 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 - vrf_name = login['radius_vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + 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!') return None def generate(login): # calculate users encrypted password - for user in login['add_users']: - if user['password_plaintext']: - user['password_encrypted'] = crypt( - user['password_plaintext'], METHOD_SHA512) - user['password_plaintext'] = '' - - # remove old plaintext password and set new encrypted password - env = os.environ.copy() - env['vyos_libexec_dir'] = '/usr/libexec/vyos' - - call("/opt/vyatta/sbin/my_delete system login user '{name}' " - "authentication plaintext-password" - .format(**user), env=env) - - call("/opt/vyatta/sbin/my_set system login user '{name}' " - "authentication encrypted-password '{password_encrypted}'" - .format(**user), env=env) - - else: - try: - if getspnam(user['name']).sp_pwdp == user['password_encrypted']: - # 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. - user['password_encrypted'] = '' - except: - pass - - if len(login['radius_server']) > 0: - render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', - login) - - uid = getpwnam('root').pw_uid - gid = getpwnam('root').pw_gid - os.chown(radius_config_file, uid, gid) - chmod_600(radius_config_file) + if 'user' in login: + for user, user_config in login['user'].items(): + tmp = dict_search('authentication.plaintext_password', user_config) + if tmp: + encrypted_password = crypt(tmp, METHOD_SHA512) + 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'] = '/usr/libexec/vyos' + + call(f"/opt/vyatta/sbin/my_delete system login user '{user}' " + "authentication plaintext-password", env=env) + + call(f"/opt/vyatta/sbin/my_set system login user '{user}' " + "authentication encrypted-password '{encrypted_password}'", env=env) + else: + try: + if getspnam(user).sp_pwdp == 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 + + if 'radius' in login: + render(radius_config_file, 'login/pam_radius_auth.conf.tmpl', login, + permission=0o600, user='root', group='root') else: if os.path.isfile(radius_config_file): os.unlink(radius_config_file) @@ -270,95 +188,72 @@ def generate(login): def apply(login): - for user in login['add_users']: - # make new user using vyatta shell and make home directory (-m), - # default group of 100 (users) - command = "useradd -m -N" - # check if user already exists: - if user['name'] in get_local_users(): - # update existing account - command = "usermod" - - # all accounts use /bin/vbash - command += " -s /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 - if user['password_encrypted']: - command += " -p '{}'".format(user['password_encrypted']) - - if user['full_name']: - command += " -c '{}'".format(user['full_name']) - - if user['home_dir']: - command += " -d '{}'".format(user['home_dir']) - - command += " -G frrvty,vyattacfg,sudo,adm,dip,disk" - command += " {}".format(user['name']) - - try: - cmd(command) - - uid = getpwnam(user['name']).pw_uid - gid = getpwnam(user['name']).pw_gid - - # we should not rely on the value stored in user['home_dir'], as a - # crazy user will choose username root or any other system user - # which will fail. Should we deny using root at all? - home_dir = getpwnam(user['name']).pw_dir - - # install ssh keys - ssh_key_dir = home_dir + '/.ssh' - if not os.path.isdir(ssh_key_dir): - os.mkdir(ssh_key_dir) - os.chown(ssh_key_dir, uid, gid) - chmod_755(ssh_key_dir) - - ssh_key_file = ssh_key_dir + '/authorized_keys' - with open(ssh_key_file, 'w') as f: - f.write("# Automatically generated by VyOS\n") - f.write("# Do not edit, all changes will be lost\n") - - for id in user['public_keys']: - line = '' - if id['options']: - line = '{} '.format(id['options']) - - line += '{} {} {}\n'.format(id['type'], - id['key'], id['name']) - f.write(line) - - os.chown(ssh_key_file, uid, gid) - chmod_600(ssh_key_file) - - except Exception as e: - print(e) - raise ConfigError('Adding user "{name}" raised exception' - .format(**user)) - - for user in login['del_users']: - try: - # Logout user if he is logged in - if user in list(set([tmp[0] for tmp in users()])): - print('{} is logged in, forcing logout'.format(user)) - call('pkill -HUP -u {}'.format(user)) - - # Remove user account but leave home directory to be safe - call(f'userdel -r {user}', stderr=DEVNULL) - - except Exception as e: - raise ConfigError(f'Deleting user "{user}" raised exception: {e}') + 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 -m -N' + # check if user already exists: + if user in get_local_users(): + # update existing account + command = 'usermod' + + # all accounts use /bin/vbash + command += ' -s /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" -p '{tmp}'" + + tmp = dict_search('full_name', user_config) + if tmp: command += f" -c '{tmp}'" + + tmp = dict_search('home_directory', user_config) + if tmp: command += f" -d '{tmp}'" + else: command += f" -d '/home/{user}'" + + command += f' -G 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 + render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.tmpl', + user_config, permission=0o600, user=user, group='users') + + except Exception as e: + raise ConfigError(f'Adding user "{user}" raised exception: "{e}"') + + if 'rm_users' in login: + for user in login['rm_users']: + try: + # 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!') + call(f'pkill -HUP -u {user}') + + # Remove user account but leave home directory to be safe + call(f'userdel -r {user}', stderr=DEVNULL) + + except Exception as e: + raise ConfigError(f'Deleting user "{user}" raised exception: {e}') # # RADIUS configuration # - if len(login['radius_server']) > 0: - try: - env = os.environ.copy() - env['DEBIAN_FRONTEND'] = 'noninteractive' + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' + try: + if 'radius' in login: # Enable RADIUS in PAM - cmd("pam-auth-update --package --enable radius", env=env) - - # Make NSS system aware of RADIUS, too + cmd('pam-auth-update --package --enable radius', env=env) + # Make NSS system aware of RADIUS + # This fancy snipped was copied from old Vyatta code command = "sed -i -e \'/\smapname/b\' \ -e \'/^passwd:/s/\s\s*/&mapuid /\' \ -e \'/^passwd:.*#/s/#.*/mapname &/\' \ @@ -366,31 +261,20 @@ def apply(login): -e \'/^group:.*#/s/#.*/ mapname &/\' \ -e \'/^group:[^#]*$/s/: */&mapname /\' \ /etc/nsswitch.conf" - - cmd(command) - - except Exception as e: - raise ConfigError('RADIUS configuration failed: {}'.format(e)) - - else: - try: - env = os.environ.copy() - env['DEBIAN_FRONTEND'] = 'noninteractive' - + else: # Disable RADIUS in PAM - cmd("pam-auth-update --package --remove radius", env=env) - + cmd('pam-auth-update --package --remove radius', env=env) + # Drop RADIUS from NSS NSS system + # This fancy snipped was copied from old Vyatta code command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \ -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \ -e \'s/[ \t]*$//\' \ /etc/nsswitch.conf" - cmd(command) - - except Exception as e: - raise ConfigError( - 'Removing RADIUS configuration failed.\n{}'.format(e)) + cmd(command) + except Exception as e: + raise ConfigError(f'RADIUS configuration failed: {e}') return None |