From 96f5fae930b8213c199069c7aab079c6fb9cd334 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 27 Jan 2020 20:57:45 +0100 Subject: login: T1948: initial rewrite in XML/Python --- interface-definitions/system-login.xml.in | 176 ++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 interface-definitions/system-login.xml.in (limited to 'interface-definitions') diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in new file mode 100644 index 000000000..33197d191 --- /dev/null +++ b/interface-definitions/system-login.xml.in @@ -0,0 +1,176 @@ + + + + + + + User Login + 400 + + + + + RADIUS based user authentication + + + + + RADIUS client source address + + ipv4 + TFTP IPv4 listen address + + + + + + + + + RADIUS server configuration + + + + + RADIUS shared secret key + + + + + RADIUS authentication port + + 1-65535 + Numeric IP port (default: 1812) + + + + + + + + + Timeout for RADIUS session + + 1-30 + Session timeout in seconds (default: 2) + + + + + Timeout must be between 1 and 30 seconds + + + + + + + + + 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-ed25519s) + + + + + + + + + + Full name of the user (use quotes for names with spaces) + + [^:]*$ + + Cannot use ':' in full name + + + + + Home directory + + + + + + + + + -- cgit v1.2.3 From 029cefc84a30fa9f34af58bfdc1dadaaf5a220db Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Jan 2020 19:51:44 +0100 Subject: login: T1948: add/remove local users --- interface-definitions/system-login.xml.in | 9 +-- src/conf_mode/system-login.py | 113 +++++++++++++++++++++++++----- 2 files changed, 102 insertions(+), 20 deletions(-) mode change 100644 => 100755 src/conf_mode/system-login.py (limited to 'interface-definitions') diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in index 33197d191..6e990290d 100644 --- a/interface-definitions/system-login.xml.in +++ b/interface-definitions/system-login.xml.in @@ -2,7 +2,7 @@ - + User Login 400 @@ -84,11 +84,12 @@ (\*|\!) [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}$ + \$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(../../@). + diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py old mode 100644 new mode 100755 index 2c1e4dc3e..9a2de54eb --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -17,16 +17,40 @@ import sys import os +from pwd import getpwall, getpwnam +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, 'radius_server': [], 'radius_source': '', - 'user': [] + '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() @@ -36,11 +60,13 @@ def get_config(): login['deleted'] = True return login - if conf.exists(base_level + ['radius', 'source-address']): + conf.set_level(base_level) + + if conf.exists(['radius', 'source-address']): login['radius_source'] = conf.return_value(['radius', 'source-address']) # Read in all RADIUS servers and store to list - for server in conf.list_nodes(base_level + ['radius', 'server']): + for server in conf.list_nodes(['radius', 'server']): radius = { 'address': server, 'key': '', @@ -65,11 +91,12 @@ def get_config(): login['radius_server'].append(radius) # Read in all local users and store to list - for username in conf.list_nodes(base_level + ['user']): + conf.set_level(base_level) + for username in conf.list_nodes(['user']): user = { 'name': username, 'password_plaintext': '', - 'password_encrypted': '', + 'password_encrypted': '!', 'public_keys': [], 'full_name': '', 'home_dir': '/home/' + username, @@ -84,6 +111,14 @@ def get_config(): 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 = { @@ -109,29 +144,75 @@ def get_config(): # Append individual public key to list of user keys user['public_keys'].append(key) - # set proper config level - conf.set_level(base_level + ['user', username]) - - # User real name - if conf.exists(['full-name']): - user['full_name'] = conf.return_value(['full-name']) + login['add_users'].append(user) - # User home-directory - if conf.exists(['home-directory']): - user['home_dir'] = conf.return_value(['home-directory']) return login def verify(login): + pass def generate(login): - import pprint - pprint.pprint(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'])) pass 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) + except Exception as e: + print('Adding user "{}" raised an exception'.format(user)) + + + for user in login['del_users']: + try: + # 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)) + pass if __name__ == '__main__': -- cgit v1.2.3 From 3a64047c2f1b6279de4b1ada7e87aa5c871f5604 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 2 Feb 2020 18:40:14 +0100 Subject: ogin: user: radius: T1948: use discrete configuration for each system Split combined XML/Python code to individual code for local user accounts and RADIUS authenticated accounts. --- interface-definitions/system-login-radius.xml.in | 67 +++++ interface-definitions/system-login-user.xml.in | 121 ++++++++++ interface-definitions/system-login.xml.in | 177 -------------- src/conf_mode/system-login-radius.py | 138 +++++++++++ src/conf_mode/system-login-user.py | 219 +++++++++++++++++ src/conf_mode/system-login.py | 295 ----------------------- 6 files changed, 545 insertions(+), 472 deletions(-) create mode 100644 interface-definitions/system-login-radius.xml.in create mode 100644 interface-definitions/system-login-user.xml.in delete mode 100644 interface-definitions/system-login.xml.in create mode 100755 src/conf_mode/system-login-radius.py create mode 100755 src/conf_mode/system-login-user.py delete mode 100755 src/conf_mode/system-login.py (limited to 'interface-definitions') diff --git a/interface-definitions/system-login-radius.xml.in b/interface-definitions/system-login-radius.xml.in new file mode 100644 index 000000000..00e85db3e --- /dev/null +++ b/interface-definitions/system-login-radius.xml.in @@ -0,0 +1,67 @@ + + + + + + + + + RADIUS based user authentication + + + + + RADIUS client source address + + ipv4 + TFTP IPv4 listen address + + + + + + + + + RADIUS server configuration + + + + + RADIUS shared secret key + + + + + RADIUS authentication port + + 1-65535 + Numeric IP port (default: 1812) + + + + + + + + + Timeout for RADIUS session + + 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 new file mode 100644 index 000000000..970bcf799 --- /dev/null +++ b/interface-definitions/system-login-user.xml.in @@ -0,0 +1,121 @@ + + + + + + + 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-ed25519s) + + + + + + + + + + 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 deleted file mode 100644 index 6e990290d..000000000 --- a/interface-definitions/system-login.xml.in +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - User Login - 400 - - - - - RADIUS based user authentication - - - - - RADIUS client source address - - ipv4 - TFTP IPv4 listen address - - - - - - - - - RADIUS server configuration - - - - - RADIUS shared secret key - - - - - RADIUS authentication port - - 1-65535 - Numeric IP port (default: 1812) - - - - - - - - - Timeout for RADIUS session - - 1-30 - Session timeout in seconds (default: 2) - - - - - Timeout must be between 1 and 30 seconds - - - - - - - - - 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-ed25519s) - - - - - - - - - - Full name of the user (use quotes for names with spaces) - - [^:]*$ - - Cannot use ':' in full name - - - - - Home directory - - - - - - - - - diff --git a/src/conf_mode/system-login-radius.py b/src/conf_mode/system-login-radius.py new file mode 100755 index 000000000..8f5d7bc36 --- /dev/null +++ b/src/conf_mode/system-login-radius.py @@ -0,0 +1,138 @@ +#!/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 +# 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']): + radius = { + 'address': server, + 'key': '', + 'port': '1812', + 'timeout': '2' + } + conf.set_level(base_level + ['server', server]) + + # RADIUS shared secret + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + # RADIUS authentication port + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + # RADIUS session timeout + if conf.exists(['timeout']): + radius['timeout'] = conf.return_value(['timeout']) + + # Append individual RADIUS server configuration to global server list + radius['server'].append(radius) + + 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: + # Enable RADIUS in PAM + os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") + else: + # Disable RADIUS in PAM + os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") + + 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..3317f87d8 --- /dev/null +++ b/src/conf_mode/system-login-user.py @@ -0,0 +1,219 @@ +#!/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 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): + 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)) + + for user in login['del_users']: + try: + # 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/conf_mode/system-login.py b/src/conf_mode/system-login.py deleted file mode 100755 index 4f741d121..000000000 --- a/src/conf_mode/system-login.py +++ /dev/null @@ -1,295 +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 grp import getgrnam -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 - -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 radius_server -%} -{% for s in radius_server -%} -{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source -%}{{ radius_source }}{% endif %} -{% endfor -%} - -priv-lvl 15 -mapped_priv_user radius_priv_user -{% endif %} - -""" - -default_config_data = { - 'deleted': False, - 'radius_server': [], - 'radius_source': '', - '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'] - - if not conf.exists(base_level): - login['deleted'] = True - return login - - conf.set_level(base_level) - - if conf.exists(['radius', 'source-address']): - login['radius_source'] = conf.return_value(['radius', 'source-address']) - - # Read in all RADIUS servers and store to list - for server in conf.list_nodes(['radius', 'server']): - radius = { - 'address': server, - 'key': '', - 'port': '1812', - 'timeout': '2' - } - conf.set_level(base_level + ['radius', 'server', server]) - - # RADIUS shared secret - if conf.exists(['key']): - radius['key'] = conf.return_value(['key']) - - # RADIUS authentication port - if conf.exists(['port']): - radius['port'] = conf.return_value(['port']) - - # RADIUS session timeout - if conf.exists(['timeout']): - radius['timeout'] = conf.return_value(['timeout']) - - # Append individual RADIUS server configuration to global server list - login['radius_server'].append(radius) - - # Read in all local users and store to list - conf.set_level(base_level) - for username in conf.list_nodes(['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) - - - return login - -def verify(login): - 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'])) - - # - # RADIUS - # - 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: - os.unlink(radius_config_file) - - pass - -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)) - - for user in login['del_users']: - try: - # 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)) - - # - # RADIUS - # - if len(login['radius_server']) > 0: - # Enable RADIUS in PAM - os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") - else: - # Disable RADIUS in PAM - os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") - - pass - -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