From c912b51e2aec383b4b0b3293b77d9305a1b8dd38 Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@jrother.eu>
Date: Sun, 9 Jan 2022 19:44:57 +0100
Subject: [PATCH] Regex filter option for groups (--group-filter-regex)

---
 debian/contrib/uffd-ldapd.conf |  1 +
 uffd-ldapd                     | 20 ++++++++++++++++----
 2 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/debian/contrib/uffd-ldapd.conf b/debian/contrib/uffd-ldapd.conf
index 03a8fa0..1ee50d0 100644
--- a/debian/contrib/uffd-ldapd.conf
+++ b/debian/contrib/uffd-ldapd.conf
@@ -9,3 +9,4 @@
 #SERVER_BIND_PASSWORD="SECRET-BIND-PASSWORD"
 
 #SERVER_CACHE_TTL="60"
+#SERVER_GROUP_FILTER_REGEX=""
diff --git a/uffd-ldapd b/uffd-ldapd
index 0b64cf4..10fad3f 100755
--- a/uffd-ldapd
+++ b/uffd-ldapd
@@ -4,6 +4,7 @@ import sys
 import socketserver
 import logging
 import socket
+import re
 
 import click
 import requests
@@ -89,6 +90,7 @@ class UffdLDAPRequestHandler(ldapserver.LDAPRequestHandler):
 	api = None
 	dn_base = None
 	bind_password = None # if None anonymous reads are allowed
+	group_filter_regex = None
 
 	def do_bind_simple_authenticated(self, dn, password):
 		dn = self.subschema.DN.from_str(dn)
@@ -186,6 +188,8 @@ class UffdLDAPRequestHandler(ldapserver.LDAPRequestHandler):
 				if value.is_direct_child_of(self.subschema.DN(self.dn_base, ou='groups')) and value.object_attribute == 'cn':
 					request_params = {'group': normalize_group_name(value.object_value)}
 					break
+		if 'group' in request_params and not self.group_filter_regex.match(request_params['group']):
+			return
 		for user in self.api.get_users(**request_params):
 			yield template.create_entry(user['loginname'],
 				cn=[user['displayname']],
@@ -195,7 +199,9 @@ class UffdLDAPRequestHandler(ldapserver.LDAPRequestHandler):
 				mail=[user['email']],
 				uid=[user['loginname']],
 				uidNumber=[user['id']],
-				memberOf=[self.subschema.DN(self.subschema.DN(self.dn_base, ou='groups'), cn=group) for group in user['groups']],
+				memberOf=[self.subschema.DN(self.subschema.DN(self.dn_base, ou='groups'), cn=group)
+				          for group in user['groups']
+				          if self.group_filter_regex.match(group)],
 			)
 
 	def do_search_groups(self, baseobj, scope, filterobj):
@@ -220,20 +226,25 @@ class UffdLDAPRequestHandler(ldapserver.LDAPRequestHandler):
 				if value.is_direct_child_of(self.subschema.DN(self.dn_base, ou='users')) and value.object_attribute == 'uid':
 					request_params = {'member': normalize_user_loginname(value.object_value)}
 					break
+		if 'name' in request_params and not self.group_filter_regex.match(request_params['name']):
+			return
 		for group in self.api.get_groups(**request_params):
+			if not self.group_filter_regex.match(group['name']):
+				continue
 			yield template.create_entry(group['name'],
 				cn=[group['name']],
 				gidNumber=[group['id']],
 				uniqueMember=[self.subschema.DN(self.subschema.DN(self.dn_base, ou='users'), uid=user) for user in group['members']],
 			)
 
-def make_requesthandler(api, dn_base, bind_password=None):
+def make_requesthandler(api, dn_base, bind_password=None, group_filter_regex=None):
 	class RequestHandler(UffdLDAPRequestHandler):
 		pass
 	dn_base = RequestHandler.subschema.DN.from_str(dn_base)
 	RequestHandler.api = api
 	RequestHandler.dn_base = dn_base
 	RequestHandler.bind_password = bind_password.encode() if bind_password else None
+	RequestHandler.group_filter_regex = re.compile(group_filter_regex) if group_filter_regex else re.compile('')
 	return RequestHandler
 
 class FilenoUnixStreamServer(socketserver.UnixStreamServer):
@@ -284,7 +295,8 @@ class StdoutFilter(logging.Filter):
 @click.option('--cache-ttl', default=60, help='Time-to-live for API response caching in seconds')
 @click.option('--base-dn', required=True, help='Base DN for user, group and system objects. E.g. "dc=example,dc=com"')
 @click.option('--bind-password', help='Authentication password for the service connection to LDAP. Bind DN is always "cn=service,ou=system,BASEDN". If set, anonymous access is disabled.')
-def main(socket_address, socket_path, socket_fd, api_url, api_user, api_secret, cache_ttl, base_dn, bind_password):
+@click.option('--group-filter-regex', help='Python regular expression that group names must match for the group to be visible to LDAP clients')
+def main(socket_address, socket_path, socket_fd, api_url, api_user, api_secret, cache_ttl, base_dn, bind_password, group_filter_regex):
 	# pylint: disable=too-many-locals
 	if (socket_address is not None) \
 	   + (socket_path is not None) \
@@ -302,7 +314,7 @@ def main(socket_address, socket_path, socket_fd, api_url, api_user, api_secret,
 	root_logger.addHandler(stderr_handler)
 
 	api = UffdAPI(api_url, api_user, api_secret, cache_ttl)
-	RequestHandler = make_requesthandler(api, base_dn, bind_password)
+	RequestHandler = make_requesthandler(api, base_dn, bind_password, group_filter_regex)
 	if socket_address is not None:
 		host, port = parse_network_address(socket_address)
 		server = socketserver.ThreadingTCPServer((host, int(port)), RequestHandler)
-- 
GitLab