From 800e208c5d02896af29061b958709ac9666da08f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 25 Feb 2020 17:10:03 +0100 Subject: login: T1948: migrade local and radius configurations Splitting was not a good idea. By combining both we can create a RADIUS server XML include file which can be reused by multiple implementations to get a uniformed CLI for the users. --- interface-definitions/system-login-radius.xml.in | 80 ----- interface-definitions/system-login-user.xml.in | 117 -------- interface-definitions/system-login.xml.in | 186 ++++++++++++ src/conf_mode/system-login-radius.py | 182 ------------ src/conf_mode/system-login-user.py | 233 --------------- src/conf_mode/system-login.py | 355 +++++++++++++++++++++++ 6 files changed, 541 insertions(+), 612 deletions(-) delete mode 100644 interface-definitions/system-login-radius.xml.in delete mode 100644 interface-definitions/system-login-user.xml.in create mode 100644 interface-definitions/system-login.xml.in delete mode 100755 src/conf_mode/system-login-radius.py delete mode 100755 src/conf_mode/system-login-user.py create mode 100755 src/conf_mode/system-login.py diff --git a/interface-definitions/system-login-radius.xml.in b/interface-definitions/system-login-radius.xml.in deleted file mode 100644 index c5d081356..000000000 --- a/interface-definitions/system-login-radius.xml.in +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - RADIUS based user authentication - - - - - RADIUS client source address - - ipv4 - TFTP IPv4 listen address - - - - - - - - - RADIUS server configuration - - ipv4 - RADIUS server IPv4 address - - - - - - - - - Temporary disable this server - - - - - - Shared secret key - - - - - Authentication port - - 1-65535 - Numeric IP port (default: 1812) - - - - - - - - - Session timeout - - 1-30 - Session timeout in seconds (default: 2) - - - - - Timeout must be between 1 and 30 seconds - - - - - - - - - - - diff --git a/interface-definitions/system-login-user.xml.in b/interface-definitions/system-login-user.xml.in deleted file mode 100644 index c183e93e3..000000000 --- a/interface-definitions/system-login-user.xml.in +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - User Login - 400 - - - - - User account information - - [a-zA-Z0-9\-_\.]{1,100} - - Username contains illegal characters or\nexceeds 100 character limitation. - - - - - Password authentication - - - - - Encrypted password - - (\*|\!) - [a-zA-Z0-9\.\/]{13}$ - \$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22} - \$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43} - \$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86} - - Invalid encrypted password for $VAR(../../@). - - - - - Plaintext password used for encryption - - - - - Remote access public keys - - >identifier< - Key identifier used by ssh-keygen (usually of form user@host) - - - - - - Public key value (base64-encoded) - - - - - Optional public key options - - - - - - - ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 - - - ssh-dss - - - - ssh-rsa - - - - ecdsa-sha2-nistp256 - - - - ecdsa-sha2-nistp384 - - - - ssh-ed25519 - - - - (ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519) - - - - - - - - - - Full name of the user (use quotes for names with spaces) - - [^:]*$ - - Cannot use ':' in full name - - - - - Home directory - - - - - - - - - diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in new file mode 100644 index 000000000..fda567074 --- /dev/null +++ b/interface-definitions/system-login.xml.in @@ -0,0 +1,186 @@ + + + + + + + User Login + 400 + + + + + Local user account information + + [a-zA-Z0-9\-_\.]{1,100} + + Username contains illegal characters or\nexceeds 100 character limitation. + + + + + Password authentication + + + + + Encrypted password + + (\*|\!) + [a-zA-Z0-9\.\/]{13}$ + \$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22} + \$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43} + \$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86} + + Invalid encrypted password for $VAR(../../@). + + + + + Plaintext password used for encryption + + + + + Remote access public keys + + >identifier< + Key identifier used by ssh-keygen (usually of form user@host) + + + + + + Public key value (base64-encoded) + + + + + Optional public key options + + + + + + + ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 + + + ssh-dss + + + + ssh-rsa + + + + ecdsa-sha2-nistp256 + + + + ecdsa-sha2-nistp384 + + + + ssh-ed25519 + + + + (ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519) + + + + + + + + + + Full name of the user (use quotes for names with spaces) + + [^:]*$ + + Cannot use ':' in full name + + + + + Home directory + + + + + + + RADIUS based user authentication + + + + + RADIUS client source address + + ipv4 + TFTP IPv4 listen address + + + + + + + + + RADIUS server configuration + + ipv4 + RADIUS server IPv4 address + + + + + + + + + Temporary disable this server + + + + + + Shared secret key + + + + + Authentication port + + 1-65535 + Numeric IP port (default: 1812) + + + + + + + + + Session timeout + + 1-30 + Session timeout in seconds (default: 2) + + + + + Timeout must be between 1 and 30 seconds + + + + + + + + + + + diff --git a/src/conf_mode/system-login-radius.py b/src/conf_mode/system-login-radius.py deleted file mode 100755 index b1e7dce4e..000000000 --- a/src/conf_mode/system-login-radius.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 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 . - -import sys -import os -import jinja2 - -from pwd import getpwall, getpwnam -from stat import S_IRUSR, S_IWUSR - -from vyos.config import Config -from vyos.configdict import list_diff -from vyos import ConfigError - -radius_config_file = "/etc/pam_radius_auth.conf" -radius_config_tmpl = """ -# Automatically generated by VyOS -# RADIUS configuration file -{%- if server %} -# server[:port] shared_secret timeout (s) source_ip -{% for s in server %} -{%- if not s.disabled -%} -{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if source_address -%}{{ source_address }}{% endif %} -{% endif %} -{%- endfor %} - -priv-lvl 15 -mapped_priv_user radius_priv_user -{% endif %} - -""" - -default_config_data = { - 'server': [], - 'source_address': '', -} - -def get_local_users(): - """Returns list of dynamically allocated users (see Debian Policy Manual)""" - local_users = [] - for p in getpwall(): - username = p[0] - uid = getpwnam(username).pw_uid - if uid in range(1000, 29999): - if username not in ['radius_user', 'radius_priv_user']: - local_users.append(username) - - return local_users - -def get_config(): - radius = default_config_data - conf = Config() - base_level = ['system', 'login', 'radius'] - - if not conf.exists(base_level): - return radius - - conf.set_level(base_level) - - if conf.exists(['source-address']): - radius['source_address'] = conf.return_value(['source-address']) - - # 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' - } - conf.set_level(base_level + ['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']) - - # Append individual RADIUS server configuration to global server list - radius['server'].append(server_cfg) - - return radius - -def verify(radius): - # At lease one RADIUS server must not be disabled - if len(radius['server']) > 0: - fail = True - for server in radius['server']: - if not server['disabled']: - fail = False - if fail: - raise ConfigError('At least one RADIUS server must be active.') - - return None - -def generate(radius): - if len(radius['server']) > 0: - tmpl = jinja2.Template(radius_config_tmpl) - config_text = tmpl.render(radius) - with open(radius_config_file, 'w') as f: - f.write(config_text) - - uid = getpwnam('root').pw_uid - gid = getpwnam('root').pw_gid - os.chown(radius_config_file, uid, gid) - os.chmod(radius_config_file, S_IRUSR | S_IWUSR) - else: - os.unlink(radius_config_file) - - return None - -def apply(radius): - if len(radius['server']) > 0: - try: - # Enable RADIUS in PAM - os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") - - # Make NSS system aware of RADIUS, too - cmd = "sed -i -e \'/\smapname/b\' \ - -e \'/^passwd:/s/\s\s*/&mapuid /\' \ - -e \'/^passwd:.*#/s/#.*/mapname &/\' \ - -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \ - -e \'/^group:.*#/s/#.*/ mapname &/\' \ - -e \'/^group:[^#]*$/s/: */&mapname /\' \ - /etc/nsswitch.conf" - - os.system(cmd) - - except Exception as e: - raise ConfigError('RADIUS configuration failed: {}'.format(e)) - - else: - try: - # Disable RADIUS in PAM - os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") - - cmd = "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" - - os.system(cmd) - - except Exception as e: - raise ConfigError('Removing RADIUS configuration failed'.format(e)) - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/system-login-user.py b/src/conf_mode/system-login-user.py deleted file mode 100755 index 087279dc7..000000000 --- a/src/conf_mode/system-login-user.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 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 . - -import sys -import os - -from pwd import getpwall, getpwnam -from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP -from subprocess import Popen, PIPE, STDOUT -from psutil import users - -from vyos.config import Config -from vyos.configdict import list_diff -from vyos import ConfigError - -default_config_data = { - 'deleted': False, - 'add_users': [], - 'del_users': [] -} - -def get_local_users(): - """Returns list of dynamically allocated users (see Debian Policy Manual)""" - local_users = [] - for p in getpwall(): - username = p[0] - uid = getpwnam(username).pw_uid - if uid in range(1000, 29999): - if username not in ['radius_user', 'radius_priv_user']: - local_users.append(username) - - return local_users - - -def get_crypt_pw(password): - command = '/usr/bin/mkpasswd --method=sha-512 {}'.format(password) - p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - return tmp.decode() - - -def get_config(): - login = default_config_data - conf = Config() - base_level = ['system', 'login', 'user'] - - # 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 = { - 'name': username, - 'password_plaintext': '', - 'password_encrypted': '!', - 'public_keys': [], - 'full_name': '', - 'home_dir': '/home/' + username, - } - conf.set_level(base_level + [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 + [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) - - # 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)) - - # 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] - - 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)) - - pass - -def generate(login): - # calculate users encrypted password - for user in login['add_users']: - if user['password_plaintext']: - user['password_encrypted'] = get_crypt_pw(user['password_plaintext']) - user['password_plaintext'] = '' - - # remove old plaintext password - # and set new encrypted password - os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password '' >/dev/null".format(user['name'])) - os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted'])) - - return None - -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) - cmd = "useradd -m -N" - # check if user already exists: - if user['name'] in get_local_users(): - # update existing account - cmd = "usermod" - - # 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 - cmd += " -p '{}'".format(user['password_encrypted']) - cmd += " -s /bin/vbash" - if user['full_name']: - cmd += " -c '{}'".format(user['full_name']) - - if user['home_dir']: - cmd += " -d '{}'".format(user['home_dir']) - - cmd += " -G frrvty,vyattacfg,sudo,adm,dip,disk" - cmd += " {}".format(user['name']) - - try: - os.system(cmd) - - uid = getpwnam(user['name']).pw_uid - gid = getpwnam(user['name']).pw_gid - - # install ssh keys - key_dir = '{}/.ssh'.format(user['home_dir']) - if not os.path.isdir(key_dir): - os.mkdir(key_dir) - os.chown(key_dir, uid, gid) - os.chmod(key_dir, S_IRWXU | S_IRGRP | S_IXGRP) - - key_file = key_dir + '/authorized_keys'; - with open(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(key_file, uid, gid) - os.chmod(key_file, S_IRUSR | S_IWUSR) - - except Exception as e: - raise ConfigError('Adding user "{}" raised an exception: {}'.format(user['name'], e)) - - 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)) - os.system('pkill -HUP -u {}'.format(user)) - - # Remove user account but leave home directory to be safe - os.system('userdel -r {} 2>/dev/null'.format(user)) - - except Exception as e: - raise ConfigError('Deleting user "{}" raised an exception: {}'.format(user, e)) - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py new file mode 100755 index 000000000..23152fee0 --- /dev/null +++ b/src/conf_mode/system-login.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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 . + +import sys +import os +import jinja2 + +from pwd import getpwall, getpwnam +from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP +from subprocess import Popen, PIPE, STDOUT +from psutil import users + +from vyos.config import Config +from vyos.configdict import list_diff +from vyos import ConfigError + +radius_config_file = "/etc/pam_radius_auth.conf" +radius_config_tmpl = """ +# Automatically generated by VyOS +# RADIUS configuration file +{%- if radius_server %} +# server[:port] shared_secret timeout (s) source_ip +{% for s in radius_server %} +{%- if not s.disabled -%} +{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source_address -%}{{ radius_source_address }}{% endif %} +{% endif %} +{%- endfor %} + +priv-lvl 15 +mapped_priv_user radius_priv_user +{% endif %} + +""" + +default_config_data = { + 'deleted': False, + 'add_users': [], + 'del_users': [], + 'radius_server': [], + 'radius_source_address': '', +} + +def get_local_users(): + """Returns list of dynamically allocated users (see Debian Policy Manual)""" + local_users = [] + for p in getpwall(): + username = p[0] + uid = getpwnam(username).pw_uid + if uid in range(1000, 29999): + if username not in ['radius_user', 'radius_priv_user']: + local_users.append(username) + + return local_users + + +def get_crypt_pw(password): + command = '/usr/bin/mkpasswd --method=sha-512 {}'.format(password) + p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) + tmp = p.communicate()[0].strip() + return tmp.decode() + + +def get_config(): + login = default_config_data + 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']) + + # 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' + } + 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']) + + # Append individual RADIUS server configuration to global server list + login['radius_server'].append(server_cfg) + + # 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)) + + # 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] + + + 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)) + + # At lease one RADIUS server must not be disabled + if len(login['radius_server']) > 0: + fail = True + for server in login['radius_server']: + if not server['disabled']: + fail = False + if fail: + raise ConfigError('At least one RADIUS server must be active.') + + + return None + +def generate(login): + # calculate users encrypted password + for user in login['add_users']: + if user['password_plaintext']: + user['password_encrypted'] = get_crypt_pw(user['password_plaintext']) + user['password_plaintext'] = '' + + # remove old plaintext password + # and set new encrypted password + os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password '' >/dev/null".format(user['name'])) + os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted'])) + + if len(login['radius_server']) > 0: + tmpl = jinja2.Template(radius_config_tmpl) + config_text = tmpl.render(login) + with open(radius_config_file, 'w') as f: + f.write(config_text) + + uid = getpwnam('root').pw_uid + gid = getpwnam('root').pw_gid + os.chown(radius_config_file, uid, gid) + os.chmod(radius_config_file, S_IRUSR | S_IWUSR) + else: + if os.path.isfile(radius_config_file): + os.unlink(radius_config_file) + + return None + +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) + cmd = "useradd -m -N" + # check if user already exists: + if user['name'] in get_local_users(): + # update existing account + cmd = "usermod" + + # 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 + cmd += " -p '{}'".format(user['password_encrypted']) + cmd += " -s /bin/vbash" + if user['full_name']: + cmd += " -c '{}'".format(user['full_name']) + + if user['home_dir']: + cmd += " -d '{}'".format(user['home_dir']) + + cmd += " -G frrvty,vyattacfg,sudo,adm,dip,disk" + cmd += " {}".format(user['name']) + + try: + os.system(cmd) + + uid = getpwnam(user['name']).pw_uid + gid = getpwnam(user['name']).pw_gid + + # install ssh keys + key_dir = '{}/.ssh'.format(user['home_dir']) + if not os.path.isdir(key_dir): + os.mkdir(key_dir) + os.chown(key_dir, uid, gid) + os.chmod(key_dir, S_IRWXU | S_IRGRP | S_IXGRP) + + key_file = key_dir + '/authorized_keys'; + with open(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(key_file, uid, gid) + os.chmod(key_file, S_IRUSR | S_IWUSR) + + except Exception as e: + raise ConfigError('Adding user "{}" raised an exception: {}'.format(user['name'], e)) + + 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)) + os.system('pkill -HUP -u {}'.format(user)) + + # Remove user account but leave home directory to be safe + os.system('userdel -r {} 2>/dev/null'.format(user)) + + except Exception as e: + raise ConfigError('Deleting user "{}" raised an exception: {}'.format(user, e)) + + # + # RADIUS configuration + # + if len(login['radius_server']) > 0: + try: + # Enable RADIUS in PAM + os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") + + # Make NSS system aware of RADIUS, too + cmd = "sed -i -e \'/\smapname/b\' \ + -e \'/^passwd:/s/\s\s*/&mapuid /\' \ + -e \'/^passwd:.*#/s/#.*/mapname &/\' \ + -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \ + -e \'/^group:.*#/s/#.*/ mapname &/\' \ + -e \'/^group:[^#]*$/s/: */&mapname /\' \ + /etc/nsswitch.conf" + + os.system(cmd) + + except Exception as e: + raise ConfigError('RADIUS configuration failed: {}'.format(e)) + + else: + try: + # Disable RADIUS in PAM + os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") + + cmd = "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" + + os.system(cmd) + + except Exception as e: + raise ConfigError('Removing RADIUS configuration failed'.format(e)) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3