diff --git a/docs/web/authentication.md b/docs/web/authentication.md index a4e08965e0..ca09ed364c 100644 --- a/docs/web/authentication.md +++ b/docs/web/authentication.md @@ -15,6 +15,7 @@ Table of Contents * [PAM authentication](#pam-authentication) * [LDAP authentication](#ldap-authentication) * [Configuration options](#configuration-options) + * Membership in custom groups with [regex_groups](#regex_groups-authentication) * [Client-side configuration](#client-side-configuration) * [Web-browser client](#web-browser-client) * [Command-line client](#command-line-client) @@ -291,6 +292,32 @@ servers as it can elongate the authentication process. } ~~~ +## Membership in custom groups with regex_groups + +Many regular expressions can be listed to define a group. Please note that the +regular expressions are searched in the whole username string, so they should +be properly anchored if you want to match only in the beginning or in the +end. Regular expression matching follows the rules of Python's +[re.search()](https://docs.python.org/3/library/re.html). + +The following example will create a group named `everybody` that contains +every user regardless of the authentication method, and a group named `admins` +that contains the user `admin` and all usernames starting with `admin_` or +ending with `_admin`. + +~~~{.json} +"regex_groups": { + "enabled" : true, + "groups" : { + "everybody" : [ ".*" ], + "admins" : [ "^admin$", "^admin_", "_admin$" ] + } +} +~~~ + +When we manage permissions on the GUI we can give permission to these +groups. For more information [see](permissions.md#managing-permissions). + ---- # Client-side configuration diff --git a/web/server/codechecker_server/session_manager.py b/web/server/codechecker_server/session_manager.py index ddcac9ff44..592d00c123 100644 --- a/web/server/codechecker_server/session_manager.py +++ b/web/server/codechecker_server/session_manager.py @@ -14,6 +14,7 @@ import hashlib import json import os +import re import uuid from codechecker_common.logger import get_logger @@ -195,6 +196,20 @@ def __init__(self, configuration_file, root_sha, force_auth=False): # Save the root SHA into the configuration (but only in memory!) self.__auth_config['method_root'] = root_sha + self.__regex_groups_enabled = False + + # Pre-compile the regular expressions of 'regex_groups' + if 'regex_groups' in self.__auth_config: + self.__regex_groups_enabled = self.__auth_config['regex_groups'] \ + .get('enabled', False) + + regex_groups = self.__auth_config['regex_groups'] \ + .get('groups', []) + d = dict() + for group_name, regex_list in regex_groups.items(): + d[group_name] = [re.compile(r) for r in regex_list] + self.__group_regexes_compiled = d + # If no methods are configured as enabled, disable authentication. if scfg_dict['authentication'].get('enabled'): found_auth_method = False @@ -333,23 +348,22 @@ def __handle_validation(self, auth_string): This validation object contains two keys: username and groups. """ - validation = self.__try_auth_root(auth_string) - if validation: - return validation - - validation = self.__try_auth_dictionary(auth_string) - if validation: - return validation - - validation = self.__try_auth_pam(auth_string) - if validation: - return validation + validation = self.__try_auth_root(auth_string) \ + or self.__try_auth_dictionary(auth_string) \ + or self.__try_auth_pam(auth_string) \ + or self.__try_auth_ldap(auth_string) + if not validation: + return False - validation = self.__try_auth_ldap(auth_string) - if validation: - return validation + # If a validation method is enabled and regex_groups is enabled too, + # we will extend the 'groups'. + extra_groups = self.__try_regex_groups(validation['username']) + if extra_groups: + already_groups = set(validation['groups']) + validation['groups'] = list(already_groups | extra_groups) - return False + LOG.debug('User validation details: %s', str(validation)) + return validation def __is_method_enabled(self, method): return method not in UNSUPPORTED_METHODS and \ @@ -480,6 +494,23 @@ def __update_groups(self, user_name, groups): return False + def __try_regex_groups(self, username): + """ + Return a set of groups that the user belongs to, depending on whether + the username matches the regular expression of the group. + + """ + if not self.__regex_groups_enabled: + return set() + + matching_groups = set() + for group_name, regex_list in self.__group_regexes_compiled.items(): + for r in regex_list: + if re.search(r, username): + matching_groups.add(group_name) + + return matching_groups + @staticmethod def get_user_name(auth_string): return auth_string.split(':')[0] diff --git a/web/server/config/server_config.json b/web/server/config/server_config.json index 1f7433438c..364273e830 100644 --- a/web/server/config/server_config.json +++ b/web/server/config/server_config.json @@ -48,6 +48,12 @@ "groups": [ "adm", "cc-users" ] + }, + "regex_groups": { + "enabled" : false, + "groups" : { + "admins_custom_group" : [ "^admin$", "^admin_", "_admin$" ] + } } } } diff --git a/web/tests/functional/authentication/test_authentication.py b/web/tests/functional/authentication/test_authentication.py index 19a875adf3..523ce45357 100644 --- a/web/tests/functional/authentication/test_authentication.py +++ b/web/tests/functional/authentication/test_authentication.py @@ -252,6 +252,57 @@ def test_group_auth(self): result = auth_client.destroySession() self.assertTrue(result, "Server did not allow us to destroy session.") + def test_regex_groups(self): + auth_client = env.setup_auth_client(self._test_workspace, + session_token='_PROHIBIT') + # First login as root. + self.sessionToken = auth_client.performLogin("Username:Password", + "root:root") + self.assertIsNotNone(self.sessionToken, + "root was unable to login!") + + # Then give SUPERUSER privs to admins_custom_group. + authd_auth_client = \ + env.setup_auth_client(self._test_workspace, + session_token=self.sessionToken) + ret = authd_auth_client.addPermission(Permission.SUPERUSER, + "admins_custom_group", + True, None) + self.assertTrue(ret) + + result = auth_client.destroySession() + self.assertTrue(result, "Server did not allow us to destroy session.") + + # Login as a user who is in admins_custom_group. + sessionToken = auth_client.performLogin("Username:Password", + "regex_admin:blah") + self.assertIsNotNone(sessionToken, + "Valid credentials didn't give us a token!") + + # Do something privileged. + client = env.setup_viewer_client(self._test_workspace, + session_token=sessionToken) + self.assertIsNotNone(client.allowsStoringAnalysisStatistics(), + "Privileged call failed.") + + result = auth_client.destroySession() + self.assertTrue(result, "Server did not allow us to destroy session.") + + # Finally try to do the same with an unprivileged user. + sessionToken = auth_client.performLogin("Username:Password", + "john:doe") + self.assertIsNotNone(sessionToken, + "Valid credentials didn't give us a token!") + + client = env.setup_viewer_client(self._test_workspace, + session_token=sessionToken) + self.assertFalse(client.allowsStoringAnalysisStatistics(), + "Privileged call from unprivileged user" + " did not fail!") + + result = auth_client.destroySession() + self.assertTrue(result, "Server did not allow us to destroy session.") + def test_personal_access_tokens(self): """ Test personal access token commands. """ codechecker_cfg = self._test_cfg['codechecker_cfg'] diff --git a/web/tests/libtest/env.py b/web/tests/libtest/env.py index 79dcbac308..96b6880167 100644 --- a/web/tests/libtest/env.py +++ b/web/tests/libtest/env.py @@ -353,9 +353,10 @@ def enable_auth(workspace): scfg_dict["authentication"]["method_dictionary"]["enabled"] = True scfg_dict["authentication"]["method_dictionary"]["auths"] = \ ["cc:test", "john:doe", "admin:admin123", "colon:my:password", - "admin_group_user:admin123"] + "admin_group_user:admin123", "regex_admin:blah"] scfg_dict["authentication"]["method_dictionary"]["groups"] = \ {"admin_group_user": ["admin_GROUP"]} + scfg_dict["authentication"]["regex_groups"]["enabled"] = True with open(server_cfg_file, 'w', encoding="utf-8", errors="ignore") as scfg: