From a1ffb5e73760e0caaca2deb8fc5a18840f968f1c Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Tue, 4 Apr 2023 14:10:56 +0000
Subject: T5145: Add maximum number of all logins on system

maxsyslogins
    maximum number of all logins on system; user is not
    allowed to log-in if total number of all user logins is
    greater than specified number (this limit does not apply
    to user with uid=0)

set system login max-login-session 2
---
 data/templates/login/limits.j2             |  5 +++++
 interface-definitions/system-login.xml.in  | 13 +++++++++++++
 smoketest/scripts/cli/test_system_login.py | 23 ++++++++++++++++++++++-
 src/conf_mode/system-login.py              | 14 +++++++++++++-
 4 files changed, 53 insertions(+), 2 deletions(-)
 create mode 100644 data/templates/login/limits.j2

diff --git a/data/templates/login/limits.j2 b/data/templates/login/limits.j2
new file mode 100644
index 000000000..5e2c11f35
--- /dev/null
+++ b/data/templates/login/limits.j2
@@ -0,0 +1,5 @@
+# Generated by /usr/libexec/vyos/conf_mode/system-login.py
+
+{% if max_login_session is vyos_defined %}
+* - maxsyslogins {{ max_login_session }}
+{% endif %}
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index b00741ffe..258913929 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -225,6 +225,19 @@
               #include <include/interface/vrf.xml.i>
             </children>
           </node>
+          <leafNode name="max-login-session">
+            <properties>
+              <help>Maximum number of all login sessions</help>
+              <valueHelp>
+                <format>u32:1-65536</format>
+                <description>Maximum number of all login sessions</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 1-65536"/>
+              </constraint>
+              <constraintErrorMessage>Maximum logins must be between 1 and 65536</constraintErrorMessage>
+            </properties>
+          </leafNode>
           <leafNode name="timeout">
             <properties>
               <help>Session timeout</help>
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index 6006fe0f6..a1d2ba2ad 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2022 VyOS maintainers and contributors
+# Copyright (C) 2019-2023 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
@@ -264,5 +264,26 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
         tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
         self.assertTrue(tmp)
 
+    def test_system_login_max_login_session(self):
+        max_logins = '2'
+        timeout = '600'
+
+        self.cli_set(base_path + ['max-login-session', max_logins])
+
+        # 'max-login-session' must be only with 'timeout' option
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+
+        self.cli_set(base_path + ['timeout', timeout])
+
+        self.cli_commit()
+
+        security_limits = read_file('/etc/security/limits.d/10-vyos.conf')
+        self.assertIn(f'* - maxsyslogins {max_logins}', security_limits)
+
+        self.cli_delete(base_path + ['timeout'])
+        self.cli_delete(base_path + ['max-login-session'])
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index d15fe399d..fbb013cf3 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2022 VyOS maintainers and contributors
+# Copyright (C) 2020-2023 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
@@ -40,6 +40,7 @@ from vyos import airbag
 airbag.enable()
 
 autologout_file = "/etc/profile.d/autologout.sh"
+limits_file = "/etc/security/limits.d/10-vyos.conf"
 radius_config_file = "/etc/pam_radius_auth.conf"
 
 # LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec
@@ -164,6 +165,9 @@ def verify(login):
             if ipv6_count > 1:
                 raise ConfigError('Only one IPv6 source-address can be set!')
 
+    if 'max_login_session' in login and 'timeout' not in login:
+        raise ConfigError('"login timeout" must be configured!')
+
     return None
 
 
@@ -226,6 +230,14 @@ def generate(login):
         if os.path.isfile(radius_config_file):
             os.unlink(radius_config_file)
 
+    # /etc/security/limits.d/10-vyos.conf
+    if 'max_login_session' in login:
+        render(limits_file, 'login/limits.j2', login,
+                   permission=0o644, user='root', group='root')
+    else:
+        if os.path.isfile(limits_file):
+            os.unlink(limits_file)
+
     if 'timeout' in login:
         render(autologout_file, 'login/autologout.j2', login,
                    permission=0o755, user='root', group='root')
-- 
cgit v1.2.3