summaryrefslogtreecommitdiff
path: root/src/conf_mode/system-login.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode/system-login.py')
-rwxr-xr-xsrc/conf_mode/system-login.py403
1 files changed, 403 insertions, 0 deletions
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
new file mode 100755
index 000000000..b1dd583b5
--- /dev/null
+++ b/src/conf_mode/system-login.py
@@ -0,0 +1,403 @@
+#!/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 os
+
+from crypt import crypt, METHOD_SHA512
+from netifaces import interfaces
+from psutil import users
+from pwd import getpwall, getpwnam
+from spwd import getspnam
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import cmd, call, DEVNULL, chmod_600, chmod_755
+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 = []
+ 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():
+ 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'])
+
+ # 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)
+
+ # 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))
+
+ 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))
+
+ # 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.')
+
+ vrf_name = login['radius_vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+ 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_set system login user '{name}' "
+ "authentication plaintext-password '{password_plaintext}'"
+ .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, trim_blocks=True)
+
+ uid = getpwnam('root').pw_uid
+ gid = getpwnam('root').pw_gid
+ os.chown(radius_config_file, uid, gid)
+ chmod_600(radius_config_file)
+ 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)
+ 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}')
+
+ #
+ # RADIUS configuration
+ #
+ if len(login['radius_server']) > 0:
+ try:
+ env = os.environ.copy()
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+ # Enable RADIUS in PAM
+ cmd("pam-auth-update --package --enable radius", env=env)
+
+ # Make NSS system aware of RADIUS, too
+ command = "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"
+
+ cmd(command)
+
+ except Exception as e:
+ raise ConfigError('RADIUS configuration failed: {}'.format(e))
+
+ else:
+ try:
+ env = os.environ.copy()
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+
+ # Disable RADIUS in PAM
+ cmd("pam-auth-update --package --remove radius", env=env)
+
+ 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))
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)