diff options
author | Christian Poessinger <christian@poessinger.com> | 2020-02-05 19:36:33 +0100 |
---|---|---|
committer | Christian Poessinger <christian@poessinger.com> | 2020-02-05 19:36:33 +0100 |
commit | 1244baf36371623b3dadcd5e98ba93a74d8330bc (patch) | |
tree | 3e86dd38c062cf99516f49471a582b8506756ce9 /src | |
parent | 54f89e9265874cba975438ca21689c60fb1b0aec (diff) | |
parent | f2e52cd21e6de853067596be8448ab9fc71b4ce1 (diff) | |
download | vyos-1x-1244baf36371623b3dadcd5e98ba93a74d8330bc.tar.gz vyos-1x-1244baf36371623b3dadcd5e98ba93a74d8330bc.zip |
Merge branch 't1948-system-login' of github.com:c-po/vyos-1x into current
* 't1948-system-login' of github.com:c-po/vyos-1x:
radius: T1948: add libnss-mapname support
radius: T1948: rename server dictionary
radius: T1948: supply PAM configuration template
user: T1948: fix system user creation
ogin: user: radius: T1948: use discrete configuration for each system
login: T1948: remove obsolete config nodes "group" and "level"
login: T1948: SSH keys can only be added after user has been created
login: T1948: initial support for RADIUS configuration
login: T1948: support for SSH keys
login: T1948: add/remove local users
login: T1948: initial rewrite in XML/Python
options: T1919: remove broken comment
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/system-login-radius.py | 163 | ||||
-rwxr-xr-x | src/conf_mode/system-login-user.py | 221 | ||||
-rwxr-xr-x | src/migration-scripts/system/14-to-15 | 1 | ||||
-rwxr-xr-x | src/migration-scripts/system/15-to-16 | 55 | ||||
-rw-r--r-- | src/pam-configs/radius | 14 |
5 files changed, 453 insertions, 1 deletions
diff --git a/src/conf_mode/system-login-radius.py b/src/conf_mode/system-login-radius.py new file mode 100755 index 000000000..52010b6ea --- /dev/null +++ b/src/conf_mode/system-login-radius.py @@ -0,0 +1,163 @@ +#!/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 <http://www.gnu.org/licenses/>. + +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 +# server[:port] shared_secret timeout (s) source_ip +{% if server -%} +{% for s in server -%} +{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if source_address -%}{{ source_address }}{% 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, + 'key': '', + 'port': '1812', + 'timeout': '2' + } + conf.set_level(base_level + ['server', server]) + + # 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): + pass + +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: + print('RADIUS configuration failed') + 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: + print('Removing RADIUS configuration failed') + + 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 new file mode 100755 index 000000000..a9e194859 --- /dev/null +++ b/src/conf_mode/system-login-user.py @@ -0,0 +1,221 @@ +#!/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 <http://www.gnu.org/licenses/>. + +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 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'] + + if not conf.exists(base_level): + login['deleted'] = True + return login + + # 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) + + return login + +def verify(login): + # TODO: should we be able to delete ourself? + pass + +def generate(login): + # 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] + + # 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" + + # encrypted password must be quited in '' else it won't work! + 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: + print('Adding user "{}" raised an exception: {}'.format(user['name'], e)) + + for user in login['del_users']: + try: + # TODO: check if user is logged in and force logout + # Remove user account but leave home directory to be safe + os.system('userdel {}'.format(user)) + except Exception as e: + print('Deleting user "{}" raised an exception'.format(user)) + + 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/migration-scripts/system/14-to-15 b/src/migration-scripts/system/14-to-15 index fd89ae57a..2491e3d0d 100755 --- a/src/migration-scripts/system/14-to-15 +++ b/src/migration-scripts/system/14-to-15 @@ -22,7 +22,6 @@ if not config.exists(base): # Nothing to do sys.exit(0) else: - # delete 'system ipv6 blacklist' node if config.exists(base + ['reboot-on-panic']): reboot = config.return_value(base + ['reboot-on-panic']) config.delete(base + ['reboot-on-panic']) diff --git a/src/migration-scripts/system/15-to-16 b/src/migration-scripts/system/15-to-16 new file mode 100755 index 000000000..e70893d55 --- /dev/null +++ b/src/migration-scripts/system/15-to-16 @@ -0,0 +1,55 @@ +#!/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 <http://www.gnu.org/licenses/>. + +# * remove "system login user <user> group" node, Why should be add a user to a +# 3rd party group when the system is fully managed by CLI? +# * remove "system login user <user> level" node +# This is the only privilege level left and also the default, what is the +# sense in keeping this orphaned node? + +import os +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['system', 'login', 'user'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + for user in config.list_nodes(base): + if config.exists(base + [user, 'group']): + config.delete(base + [user, 'group']) + + if config.exists(base + [user, 'level']): + config.delete(base + [user, 'level']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/pam-configs/radius b/src/pam-configs/radius new file mode 100644 index 000000000..9353de458 --- /dev/null +++ b/src/pam-configs/radius @@ -0,0 +1,14 @@ +Name: RADIUS authentication +Default: no +Priority: 257 +Auth-Type: Primary +Auth: + [authinfo_unavail=ignore success=end auth_err=bad default=ignore] pam_radius_auth.so + +Account-Type: Primary +Account: + [authinfo_unavail=ignore success=end perm_denied=bad default=ignore] pam_radius_auth.so + +Session-Type: Additional +Session: + [authinfo_unavail=ignore success=ok default=ignore] pam_radius_auth.so |