diff options
-rw-r--r-- | interface-definitions/system-login-radius.xml.in | 80 | ||||
-rw-r--r-- | interface-definitions/system-login.xml.in (renamed from interface-definitions/system-login-user.xml.in) | 75 | ||||
-rwxr-xr-x | src/conf_mode/system-login-radius.py | 182 | ||||
-rwxr-xr-x | src/conf_mode/system-login.py (renamed from src/conf_mode/system-login-user.py) | 134 |
4 files changed, 200 insertions, 271 deletions
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 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="system"> - <children> - <node name="login"> - <children> - <node name="radius" owner="${vyos_conf_scripts_dir}/system-login-radius.py"> - <properties> - <help>RADIUS based user authentication</help> - </properties> - <children> - <leafNode name="source-address"> - <properties> - <help>RADIUS client source address</help> - <valueHelp> - <format>ipv4</format> - <description>TFTP IPv4 listen address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <tagNode name="server"> - <properties> - <help>RADIUS server configuration</help> - <valueHelp> - <format>ipv4</format> - <description>RADIUS server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - <children> - <leafNode name="disable"> - <properties> - <help>Temporary disable this server</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="key"> - <properties> - <help>Shared secret key</help> - </properties> - </leafNode> - <leafNode name="port"> - <properties> - <help>Authentication port</help> - <valueHelp> - <format>1-65535</format> - <description>Numeric IP port (default: 1812)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="timeout"> - <properties> - <help>Session timeout</help> - <valueHelp> - <format>1-30</format> - <description>Session timeout in seconds (default: 2)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-30"/> - </constraint> - <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </node> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/system-login-user.xml.in b/interface-definitions/system-login.xml.in index c183e93e3..fda567074 100644 --- a/interface-definitions/system-login-user.xml.in +++ b/interface-definitions/system-login.xml.in @@ -2,15 +2,15 @@ <interfaceDefinition> <node name="system"> <children> - <node name="login"> + <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py"> <properties> <help>User Login</help> <priority>400</priority> </properties> <children> - <tagNode name="user" owner="${vyos_conf_scripts_dir}/system-login-user.py"> + <tagNode name="user"> <properties> - <help>User account information</help> + <help>Local user account information</help> <constraint> <regex>[a-zA-Z0-9\-_\.]{1,100}</regex> </constraint> @@ -110,6 +110,75 @@ </leafNode> </children> </tagNode> + <node name="radius"> + <properties> + <help>RADIUS based user authentication</help> + </properties> + <children> + <leafNode name="source-address"> + <properties> + <help>RADIUS client source address</help> + <valueHelp> + <format>ipv4</format> + <description>TFTP IPv4 listen address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <tagNode name="server"> + <properties> + <help>RADIUS server configuration</help> + <valueHelp> + <format>ipv4</format> + <description>RADIUS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Temporary disable this server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="key"> + <properties> + <help>Shared secret key</help> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Authentication port</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 1812)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="timeout"> + <properties> + <help>Session timeout</help> + <valueHelp> + <format>1-30</format> + <description>Session timeout in seconds (default: 2)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-30"/> + </constraint> + <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> </children> </node> </children> 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 <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 -{%- 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.py index 087279dc7..23152fee0 100755 --- a/src/conf_mode/system-login-user.py +++ b/src/conf_mode/system-login.py @@ -16,6 +16,7 @@ import sys import os +import jinja2 from pwd import getpwall, getpwnam from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP @@ -26,10 +27,30 @@ 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': [] + 'del_users': [], + 'radius_server': [], + 'radius_source_address': '', } def get_local_users(): @@ -55,7 +76,7 @@ def get_crypt_pw(password): def get_config(): login = default_config_data conf = Config() - base_level = ['system', 'login', 'user'] + 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 @@ -64,7 +85,7 @@ def get_config(): # All fine so far! # Read in all local users and store to list - for username in conf.list_nodes(base_level): + for username in conf.list_nodes(base_level + ['user']): user = { 'name': username, 'password_plaintext': '', @@ -73,7 +94,7 @@ def get_config(): 'full_name': '', 'home_dir': '/home/' + username, } - conf.set_level(base_level + [username]) + conf.set_level(base_level + ['user', username]) # Plaintext password if conf.exists(['authentication', 'plaintext-password']): @@ -99,7 +120,7 @@ def get_config(): 'options': '', 'type': '' } - conf.set_level(base_level + [username, 'authentication', 'public-keys', id]) + conf.set_level(base_level + ['user', username, 'authentication', 'public-keys', id]) # Public Key portion if conf.exists(['key']): @@ -118,6 +139,44 @@ def get_config(): 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']] @@ -129,6 +188,7 @@ def get_config(): # system is rebooted. login['del_users'] = [tmp for tmp in all_users if tmp not in cli_users] + return login def verify(login): @@ -136,7 +196,17 @@ def verify(login): if cur_user in login['del_users']: raise ConfigError('Attempting to delete current user: {}'.format(cur_user)) - pass + # 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 @@ -150,6 +220,20 @@ def generate(login): 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): @@ -220,6 +304,44 @@ def apply(login): 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__': |