diff --git a/policies.py b/policies.py index 6d49f3477..cd34b7c1c 100644 --- a/policies.py +++ b/policies.py @@ -617,6 +617,25 @@ def rule_check_anonymous_access_allowed(ctx, address): return "true" if address in permit_list else "false" +@rule.make(inputs=[], outputs=[0]) +def rule_check_max_connections_exceeded(ctx): + """Check if user exceeds the maximum number of connections. + + :param ctx: Combined type of a callback and rei struct + + :returns: 'true' if maximum number of connections is exceeded; otherwise 'false'. + Also returns 'false' if the maximum number of connections check has been + disabled, or if the maximum number does not apply to the present user. + """ + if not config.user_max_connections_enabled: + return "false" + elif user.name(ctx) in ['anonymous', 'rods']: + return "false" + else: + connections = user.number_of_connections(ctx) + return "false" if connections <= config.user_max_connections_number else "true" + + @rule.make(inputs=[0, 1, 2, 3, 4], outputs=[]) def pep_database_gen_query_pre(ctx, dbtype, _ctx, results, genquery_inp, genquery_out): if not is_safe_genquery_inp(genquery_inp): diff --git a/util/config.py b/util/config.py index 08d50ffad..731d131d9 100644 --- a/util/config.py +++ b/util/config.py @@ -145,6 +145,8 @@ def __repr__(self): vault_copy_backoff_time=300, vault_copy_max_retries=5, vault_copy_multithread_enabled=True, + user_max_connections_enabled=False, + user_max_connections_number=4, python3_interpreter='/usr/local/bin/python3') # }}} diff --git a/util/user.py b/util/user.py index 9d7083c1b..ed94b67e6 100644 --- a/util/user.py +++ b/util/user.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- """Utility / convenience functions for querying user info.""" -__copyright__ = 'Copyright (c) 2019-2022, Utrecht University' +__copyright__ = 'Copyright (c) 2019-2024, Utrecht University' __license__ = 'GPLv3, see LICENSE' +import subprocess from collections import namedtuple import genquery import session_vars +import log + # User is a tuple consisting of a name and a zone, which stringifies into 'user#zone'. User = namedtuple('User', ['name', 'zone']) User.__str__ = lambda self: '{}#{}'.format(*self) @@ -104,3 +107,19 @@ def name_from_id(ctx, user_id): genquery.AS_LIST, ctx): return row[0] return '' + + +def number_of_connections(ctx): + """Get number of active connections from client user.""" + connections = 0 + try: + # We don't use the -a option with the ips command, because this takes + # significantly more time, which would significantly reduce performance. + ips = subprocess.check_output(["ips"]) + username = session_vars.get_map(ctx.rei)['client_user']['user_name'] + connections = ips.count(username) + except Exception as e: + log.write(ctx, "Error: unable to determine number of user connections: " + str(e)) + return 0 + + return connections diff --git a/uuPolicies.r b/uuPolicies.r index eac661a15..7e8dcba60 100644 --- a/uuPolicies.r +++ b/uuPolicies.r @@ -119,6 +119,13 @@ pep_api_auth_request_pre(*instanceName, *comm, *request) { } } + *max_connections_exceeded = ''; + rule_check_max_connections_exceeded(*max_connections_exceeded); + if ( *max_connections_exceeded == "true" ) { + writeLine("serverLog", "Refused access for *user_name#*zone_name, max connections exceeded."); + failmsg(-1, "Refused access for *user_name#*zone_name, max connections exceeded."); + } + writeLine("serverLog", "{*user_name#*zone_name} Agent process started from *client_addr"); }