From 7e1462c21ed853b3b4a7cc347091a132f6b4162e Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 2 Feb 2020 11:25:31 +0100
Subject: options: T1919: remove broken comment

---
 src/migration-scripts/system/14-to-15 | 1 -
 1 file changed, 1 deletion(-)

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'])
-- 
cgit v1.2.3


From 96f5fae930b8213c199069c7aab079c6fb9cd334 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 27 Jan 2020 20:57:45 +0100
Subject: login: T1948: initial rewrite in XML/Python

---
 debian/control                            |   1 +
 interface-definitions/system-login.xml.in | 176 ++++++++++++++++++++++++++++++
 src/conf_mode/system-login.py             | 145 ++++++++++++++++++++++++
 3 files changed, 322 insertions(+)
 create mode 100644 interface-definitions/system-login.xml.in
 create mode 100644 src/conf_mode/system-login.py

diff --git a/debian/control b/debian/control
index 9df421977..6e59ea2fb 100644
--- a/debian/control
+++ b/debian/control
@@ -63,6 +63,7 @@ Depends: python3,
   openvpn,
   openvpn-auth-ldap,
   openvpn-auth-radius,
+  libpam-radius-auth,
   mtr-tiny,
   telnet,
   traceroute,
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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="system">
+    <children>
+      <node name="login" owner="${vyos_conf_scripts_dir}/system_login.py">
+        <properties>
+          <help>User Login</help>
+          <priority>400</priority>
+        </properties>
+        <children>
+          <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>
+                </properties>
+                <children>
+                  <leafNode name="key">
+                    <properties>
+                      <help>RADIUS shared secret key</help>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="port">
+                    <properties>
+                      <help>RADIUS 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>Timeout for RADIUS session</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>
+          <tagNode name="user">
+            <properties>
+              <help>User account information</help>
+              <constraint>
+                <regex>[a-zA-Z0-9\-_\.]{1,100}</regex>
+              </constraint>
+              <constraintErrorMessage>Username contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage>
+            </properties>
+            <children>
+              <node name="authentication">
+                <properties>
+                  <help>Password authentication</help>
+                </properties>
+                <children>
+                  <leafNode name="encrypted-password">
+                    <properties>
+                      <help>Encrypted password</help>
+                      <constraint>
+                        <regex>(\*|\!)</regex>
+                        <regex>[a-zA-Z0-9\.\/]{13}$</regex>
+                        <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}$</regex>
+                        <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}$</regex>
+                        <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}$</regex>
+                      </constraint>
+                      <constraintErrorMessage>Invalid encrypted password for $VAR(../../@).</constraintErrorMessage>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="plaintext-password">
+                    <properties>
+                      <help>Plaintext password used for encryption</help>
+                    </properties>
+                  </leafNode>
+                  <tagNode name="public-keys">
+                    <properties>
+                      <help>Remote access public keys</help>
+                      <valueHelp>
+                        <format>&gt;identifier&lt;</format>
+                        <description>Key identifier used by ssh-keygen (usually of form user@host)</description>
+                      </valueHelp>
+                    </properties>
+                    <children>
+                      <leafNode name="key">
+                        <properties>
+                          <help>Public key value (base64-encoded)</help>
+                          <completionHelp>
+                            <script>echo 'The key is usually several hundred bytes long (because of the size of the public key encoding). Use the loadkey tool to input key from a URL or file.'</script>
+                          </completionHelp>
+                        </properties>
+                      </leafNode>
+                      <leafNode name="options">
+                        <properties>
+                          <help>Optional public key options</help>
+                        </properties>
+                      </leafNode>
+                      <leafNode name="type">
+                        <properties>
+                          <help></help>
+                          <completionHelp>
+                            <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519</list>
+                          </completionHelp>
+                          <valueHelp>
+                            <format>ssh-dss</format>
+                            <description/>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ssh-rsa</format>
+                            <description/>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ecdsa-sha2-nistp256</format>
+                            <description/>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ecdsa-sha2-nistp384</format>
+                            <description/>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ssh-ed25519</format>
+                            <description/>
+                          </valueHelp>
+                          <constraint>
+                            <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519s)</regex>
+                          </constraint>
+                        </properties>
+                      </leafNode>
+                    </children>
+                  </tagNode>
+                </children>
+			  </node>
+              <leafNode name="full-name">
+                <properties>
+                  <help>Full name of the user (use quotes for names with spaces)</help>
+                  <constraint>
+                    <regex>[^:]*$</regex>
+                  </constraint>
+                  <constraintErrorMessage>Cannot use ':' in full name</constraintErrorMessage>
+                </properties>
+              </leafNode>
+              <leafNode name="home-directory">
+                <properties>
+                  <help>Home directory</help>
+                </properties>
+              </leafNode>
+            </children>
+          </tagNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
new file mode 100644
index 000000000..2c1e4dc3e
--- /dev/null
+++ b/src/conf_mode/system-login.py
@@ -0,0 +1,145 @@
+#!/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 vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+    'deleted': False,
+    'radius_server': [],
+    'radius_source': '',
+    'user': []
+}
+
+def get_config():
+    login = default_config_data
+    conf = Config()
+    base_level = ['system', 'login']
+
+    if not conf.exists(base_level):
+        login['deleted'] = True
+        return login
+
+    if conf.exists(base_level + ['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']):
+        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
+    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'])
+
+        # 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']):
+                user['key'] = conf.return_value(['key'])
+
+            # Options for individual public key
+            if conf.exists(['options']):
+                user['options'] = conf.return_value(['options'])
+
+            # Type of public key
+            if conf.exists(['type']):
+                user['type'] = conf.return_value(['type'])
+
+            # 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'])
+
+        # 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)
+
+    pass
+
+def apply(login):
+    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


From 029cefc84a30fa9f34af58bfdc1dadaaf5a220db Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
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

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 @@
 <interfaceDefinition>
   <node name="system">
     <children>
-      <node name="login" owner="${vyos_conf_scripts_dir}/system_login.py">
+      <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py">
         <properties>
           <help>User Login</help>
           <priority>400</priority>
@@ -84,11 +84,12 @@
                       <constraint>
                         <regex>(\*|\!)</regex>
                         <regex>[a-zA-Z0-9\.\/]{13}$</regex>
-                        <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}$</regex>
-                        <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}$</regex>
-                        <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}$</regex>
+                        <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}</regex>
+                        <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}</regex>
+                        <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}</regex>
                       </constraint>
                       <constraintErrorMessage>Invalid encrypted password for $VAR(../../@).</constraintErrorMessage>
+
                     </properties>
                   </leafNode>
                   <leafNode name="plaintext-password">
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 a717e1c802d958137cdc70adf44d614323438dce Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 30 Jan 2020 21:05:07 +0100
Subject: login: T1948: support for SSH keys

---
 src/conf_mode/system-login.py | 34 +++++++++++++++++++++++++++++++---
 1 file changed, 31 insertions(+), 3 deletions(-)

diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 9a2de54eb..8aa3991fd 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -18,6 +18,8 @@ import sys
 import os
 
 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
@@ -131,15 +133,15 @@ def get_config():
 
             # Public Key portion
             if conf.exists(['key']):
-                user['key'] = conf.return_value(['key'])
+                key['key'] = conf.return_value(['key'])
 
             # Options for individual public key
             if conf.exists(['options']):
-                user['options'] = conf.return_value(['options'])
+                key['options'] = conf.return_value(['options'])
 
             # Type of public key
             if conf.exists(['type']):
-                user['type'] = conf.return_value(['type'])
+                key['type'] = conf.return_value(['type'])
 
             # Append individual public key to list of user keys
             user['public_keys'].append(key)
@@ -176,6 +178,32 @@ 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']))
 
+        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)
+
     pass
 
 def apply(login):
-- 
cgit v1.2.3


From b1bb4dcc8dd9d08e0845ecd4c568511e61c594d1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 30 Jan 2020 21:45:51 +0100
Subject: login: T1948: initial support for RADIUS configuration

---
 debian/control                |  2 +-
 src/conf_mode/system-login.py | 47 ++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 45 insertions(+), 4 deletions(-)

diff --git a/debian/control b/debian/control
index 6e59ea2fb..2901f792e 100644
--- a/debian/control
+++ b/debian/control
@@ -63,7 +63,7 @@ Depends: python3,
   openvpn,
   openvpn-auth-ldap,
   openvpn-auth-radius,
-  libpam-radius-auth,
+  libpam-radius-auth (>= 1.5.0),
   mtr-tiny,
   telnet,
   traceroute,
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 8aa3991fd..3d29010b9 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -16,6 +16,7 @@
 
 import sys
 import os
+import jinja2
 
 from pwd import getpwall, getpwnam
 from grp import getgrnam
@@ -26,6 +27,21 @@ 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,
@@ -152,7 +168,6 @@ def get_config():
     return login
 
 def verify(login):
-
     pass
 
 def generate(login):
@@ -186,7 +201,7 @@ def generate(login):
         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)
+            os.chmod(key_dir, S_IRWXU | S_IRGRP | S_IXGRP)
 
         key_file = key_dir + '/authorized_keys';
         with open(key_file, 'w') as f:
@@ -202,7 +217,23 @@ def generate(login):
                 f.write(line)
 
         os.chown(key_file, uid, gid)
-        os.chmod(key_file, S_IRUSR|S_IWUSR)
+        os.chmod(key_file, S_IRUSR | S_IWUSR)
+
+    #
+    # 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
 
@@ -241,6 +272,16 @@ def apply(login):
         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__':
-- 
cgit v1.2.3


From f1726cd0d0b8e7b809576189918d6ac298983100 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 30 Jan 2020 21:47:12 +0100
Subject: login: T1948: SSH keys can only be added after user has been created

---
 src/conf_mode/system-login.py | 54 +++++++++++++++++++++----------------------
 1 file changed, 27 insertions(+), 27 deletions(-)

diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 3d29010b9..4f741d121 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -193,32 +193,6 @@ 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']))
 
-        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)
-
     #
     # RADIUS
     #
@@ -261,10 +235,36 @@ def apply(login):
 
         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
-- 
cgit v1.2.3


From 9c52edb9e2079c315af7385a85f61f21138dd5a6 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 2 Feb 2020 11:25:55 +0100
Subject: login: T1948: remove obsolete config nodes "group" and "level"

---
 src/migration-scripts/system/15-to-16 | 55 +++++++++++++++++++++++++++++++++++
 1 file changed, 55 insertions(+)
 create mode 100755 src/migration-scripts/system/15-to-16

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)
-- 
cgit v1.2.3


From 3a64047c2f1b6279de4b1ada7e87aa5c871f5604 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
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

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 @@
+<?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>
+                </properties>
+                <children>
+                  <leafNode name="key">
+                    <properties>
+                      <help>RADIUS shared secret key</help>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="port">
+                    <properties>
+                      <help>RADIUS 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>Timeout for RADIUS session</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-user.xml.in
new file mode 100644
index 000000000..970bcf799
--- /dev/null
+++ b/interface-definitions/system-login-user.xml.in
@@ -0,0 +1,121 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="system">
+    <children>
+      <node name="login">
+        <properties>
+          <help>User Login</help>
+          <priority>400</priority>
+        </properties>
+        <children>
+          <tagNode name="user" owner="${vyos_conf_scripts_dir}/system-login-user.py">
+            <properties>
+              <help>User account information</help>
+              <constraint>
+                <regex>[a-zA-Z0-9\-_\.]{1,100}</regex>
+              </constraint>
+              <constraintErrorMessage>Username contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage>
+            </properties>
+            <children>
+              <node name="authentication">
+                <properties>
+                  <help>Password authentication</help>
+                </properties>
+                <children>
+                  <leafNode name="encrypted-password">
+                    <properties>
+                      <help>Encrypted password</help>
+                      <constraint>
+                        <regex>(\*|\!)</regex>
+                        <regex>[a-zA-Z0-9\.\/]{13}$</regex>
+                        <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}</regex>
+                        <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}</regex>
+                        <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}</regex>
+                      </constraint>
+                      <constraintErrorMessage>Invalid encrypted password for $VAR(../../@).</constraintErrorMessage>
+
+                    </properties>
+                  </leafNode>
+                  <leafNode name="plaintext-password">
+                    <properties>
+                      <help>Plaintext password used for encryption</help>
+                    </properties>
+                  </leafNode>
+                  <tagNode name="public-keys">
+                    <properties>
+                      <help>Remote access public keys</help>
+                      <valueHelp>
+                        <format>&gt;identifier&lt;</format>
+                        <description>Key identifier used by ssh-keygen (usually of form user@host)</description>
+                      </valueHelp>
+                    </properties>
+                    <children>
+                      <leafNode name="key">
+                        <properties>
+                          <help>Public key value (base64-encoded)</help>
+                          <completionHelp>
+                            <script>echo 'The key is usually several hundred bytes long (because of the size of the public key encoding). Use the loadkey tool to input key from a URL or file.'</script>
+                          </completionHelp>
+                        </properties>
+                      </leafNode>
+                      <leafNode name="options">
+                        <properties>
+                          <help>Optional public key options</help>
+                        </properties>
+                      </leafNode>
+                      <leafNode name="type">
+                        <properties>
+                          <help></help>
+                          <completionHelp>
+                            <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519</list>
+                          </completionHelp>
+                          <valueHelp>
+                            <format>ssh-dss</format>
+                            <description/>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ssh-rsa</format>
+                            <description/>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ecdsa-sha2-nistp256</format>
+                            <description/>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ecdsa-sha2-nistp384</format>
+                            <description/>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ssh-ed25519</format>
+                            <description/>
+                          </valueHelp>
+                          <constraint>
+                            <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519s)</regex>
+                          </constraint>
+                        </properties>
+                      </leafNode>
+                    </children>
+                  </tagNode>
+                </children>
+			  </node>
+              <leafNode name="full-name">
+                <properties>
+                  <help>Full name of the user (use quotes for names with spaces)</help>
+                  <constraint>
+                    <regex>[^:]*$</regex>
+                  </constraint>
+                  <constraintErrorMessage>Cannot use ':' in full name</constraintErrorMessage>
+                </properties>
+              </leafNode>
+              <leafNode name="home-directory">
+                <properties>
+                  <help>Home directory</help>
+                </properties>
+              </leafNode>
+            </children>
+          </tagNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
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 @@
-<?xml version="1.0"?>
-<interfaceDefinition>
-  <node name="system">
-    <children>
-      <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py">
-        <properties>
-          <help>User Login</help>
-          <priority>400</priority>
-        </properties>
-        <children>
-          <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>
-                </properties>
-                <children>
-                  <leafNode name="key">
-                    <properties>
-                      <help>RADIUS shared secret key</help>
-                    </properties>
-                  </leafNode>
-                  <leafNode name="port">
-                    <properties>
-                      <help>RADIUS 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>Timeout for RADIUS session</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>
-          <tagNode name="user">
-            <properties>
-              <help>User account information</help>
-              <constraint>
-                <regex>[a-zA-Z0-9\-_\.]{1,100}</regex>
-              </constraint>
-              <constraintErrorMessage>Username contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage>
-            </properties>
-            <children>
-              <node name="authentication">
-                <properties>
-                  <help>Password authentication</help>
-                </properties>
-                <children>
-                  <leafNode name="encrypted-password">
-                    <properties>
-                      <help>Encrypted password</help>
-                      <constraint>
-                        <regex>(\*|\!)</regex>
-                        <regex>[a-zA-Z0-9\.\/]{13}$</regex>
-                        <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}</regex>
-                        <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}</regex>
-                        <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}</regex>
-                      </constraint>
-                      <constraintErrorMessage>Invalid encrypted password for $VAR(../../@).</constraintErrorMessage>
-
-                    </properties>
-                  </leafNode>
-                  <leafNode name="plaintext-password">
-                    <properties>
-                      <help>Plaintext password used for encryption</help>
-                    </properties>
-                  </leafNode>
-                  <tagNode name="public-keys">
-                    <properties>
-                      <help>Remote access public keys</help>
-                      <valueHelp>
-                        <format>&gt;identifier&lt;</format>
-                        <description>Key identifier used by ssh-keygen (usually of form user@host)</description>
-                      </valueHelp>
-                    </properties>
-                    <children>
-                      <leafNode name="key">
-                        <properties>
-                          <help>Public key value (base64-encoded)</help>
-                          <completionHelp>
-                            <script>echo 'The key is usually several hundred bytes long (because of the size of the public key encoding). Use the loadkey tool to input key from a URL or file.'</script>
-                          </completionHelp>
-                        </properties>
-                      </leafNode>
-                      <leafNode name="options">
-                        <properties>
-                          <help>Optional public key options</help>
-                        </properties>
-                      </leafNode>
-                      <leafNode name="type">
-                        <properties>
-                          <help></help>
-                          <completionHelp>
-                            <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519</list>
-                          </completionHelp>
-                          <valueHelp>
-                            <format>ssh-dss</format>
-                            <description/>
-                          </valueHelp>
-                          <valueHelp>
-                            <format>ssh-rsa</format>
-                            <description/>
-                          </valueHelp>
-                          <valueHelp>
-                            <format>ecdsa-sha2-nistp256</format>
-                            <description/>
-                          </valueHelp>
-                          <valueHelp>
-                            <format>ecdsa-sha2-nistp384</format>
-                            <description/>
-                          </valueHelp>
-                          <valueHelp>
-                            <format>ssh-ed25519</format>
-                            <description/>
-                          </valueHelp>
-                          <constraint>
-                            <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519s)</regex>
-                          </constraint>
-                        </properties>
-                      </leafNode>
-                    </children>
-                  </tagNode>
-                </children>
-			  </node>
-              <leafNode name="full-name">
-                <properties>
-                  <help>Full name of the user (use quotes for names with spaces)</help>
-                  <constraint>
-                    <regex>[^:]*$</regex>
-                  </constraint>
-                  <constraintErrorMessage>Cannot use ':' in full name</constraintErrorMessage>
-                </properties>
-              </leafNode>
-              <leafNode name="home-directory">
-                <properties>
-                  <help>Home directory</help>
-                </properties>
-              </leafNode>
-            </children>
-          </tagNode>
-        </children>
-      </node>
-    </children>
-  </node>
-</interfaceDefinition>
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 <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']):
+        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 <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):
+    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 <http://www.gnu.org/licenses/>.
-
-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


From 0d88ad9642ccf0b8780c063a3a77cac74dd0ee0d Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 5 Feb 2020 19:32:55 +0100
Subject: user: T1948: fix system user creation

---
 src/conf_mode/system-login-user.py | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/conf_mode/system-login-user.py b/src/conf_mode/system-login-user.py
index 3317f87d8..a9e194859 100755
--- a/src/conf_mode/system-login-user.py
+++ b/src/conf_mode/system-login-user.py
@@ -118,6 +118,7 @@ def get_config():
     return login
 
 def verify(login):
+    # TODO: should we be able to delete ourself?
     pass
 
 def generate(login):
@@ -156,16 +157,16 @@ def apply(login):
             cmd = "usermod"
 
         # encrypted password must be quited in '' else it won't work!
-        cmd += " -p '{}'".format(user['password_encrypted'])
-        cmd += " -s /bin/vbash"
+        cmd += ' -p "{}"'.format(user['password_encrypted'])
+        cmd += ' -s /bin/vbash'
         if user['full_name']:
-            cmd += " -c {}".format(user['full_name'])
+            cmd += ' -c "{}"'.format(user['full_name'])
 
         if user['home_dir']:
-            cmd += " -d '{}'".format(user['home_dir'])
+            cmd += ' -d "{}"'.format(user['home_dir'])
 
-        cmd += " -G frrvty,vyattacfg,sudo,adm,dip,disk"
-        cmd += " {}".format(user['name'])
+        cmd += ' -G frrvty,vyattacfg,sudo,adm,dip,disk'
+        cmd += ' {}'.format(user['name'])
 
         try:
             os.system(cmd)
@@ -197,10 +198,11 @@ def apply(login):
             os.chmod(key_file, S_IRUSR | S_IWUSR)
 
         except Exception as e:
-            print('Adding user "{}" raised an exception'.format(user))
+            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:
-- 
cgit v1.2.3


From 74329734d3c465675ec3650cb2b8d1cbe8ec0885 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 5 Feb 2020 19:33:18 +0100
Subject: radius: T1948: supply PAM configuration template

---
 debian/rules           |  4 ++++
 src/pam-configs/radius | 14 ++++++++++++++
 2 files changed, 18 insertions(+)
 create mode 100644 src/pam-configs/radius

diff --git a/debian/rules b/debian/rules
index d529c9b4e..144132389 100755
--- a/debian/rules
+++ b/debian/rules
@@ -74,6 +74,10 @@ override_dh_auto_install:
 	mkdir -p $(DIR)/etc
 	cp -r src/etc/* $(DIR)/etc
 
+	# Install PAM configuration snippets
+	mkdir -p $(DIR)/usr/share/pam-configs
+	cp -r src/pam-configs/* $(DIR)/usr/share/pam-configs
+
 	# Install systemd service units
 	mkdir -p $(DIR)/lib/systemd/system
 	cp -r src/systemd/* $(DIR)/lib/systemd/system
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
-- 
cgit v1.2.3


From abedc2155adad8f8df1c99b46bfba171cb14db65 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 5 Feb 2020 19:33:58 +0100
Subject: radius: T1948: rename server dictionary

---
 src/conf_mode/system-login-radius.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/conf_mode/system-login-radius.py b/src/conf_mode/system-login-radius.py
index 8f5d7bc36..515e4f637 100755
--- a/src/conf_mode/system-login-radius.py
+++ b/src/conf_mode/system-login-radius.py
@@ -73,7 +73,7 @@ def get_config():
 
     # Read in all RADIUS servers and store to list
     for server in conf.list_nodes(['server']):
-        radius = {
+        server_cfg = {
             'address': server,
             'key': '',
             'port': '1812',
@@ -83,18 +83,18 @@ def get_config():
 
         # RADIUS shared secret
         if conf.exists(['key']):
-            radius['key'] = conf.return_value(['key'])
+            server_cfg['key'] = conf.return_value(['key'])
 
         # RADIUS authentication port
         if conf.exists(['port']):
-            radius['port'] = conf.return_value(['port'])
+            server_cfg['port'] = conf.return_value(['port'])
 
         # RADIUS session timeout
         if conf.exists(['timeout']):
-            radius['timeout'] = conf.return_value(['timeout'])
+            server_cfg['timeout'] = conf.return_value(['timeout'])
 
         # Append individual RADIUS server configuration to global server list
-        radius['server'].append(radius)
+        radius['server'].append(server_cfg)
 
     return radius
 
-- 
cgit v1.2.3


From f2e52cd21e6de853067596be8448ab9fc71b4ce1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 5 Feb 2020 19:34:13 +0100
Subject: radius: T1948: add libnss-mapname support

---
 src/conf_mode/system-login-radius.py | 33 +++++++++++++++++++++++++++++----
 1 file changed, 29 insertions(+), 4 deletions(-)

diff --git a/src/conf_mode/system-login-radius.py b/src/conf_mode/system-login-radius.py
index 515e4f637..52010b6ea 100755
--- a/src/conf_mode/system-login-radius.py
+++ b/src/conf_mode/system-login-radius.py
@@ -119,11 +119,36 @@ def generate(radius):
 
 def apply(radius):
     if len(radius['server']) > 0:
-        # Enable RADIUS in PAM
-        os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius")
+        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:
-        # Disable RADIUS in PAM
-        os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius")
+        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
 
-- 
cgit v1.2.3