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: