From: Michael Tremer Date: Fri, 9 Jan 2026 15:57:40 +0000 (+0000) Subject: users: Add a simple system to access users and groups X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c24d447c76724dbda81291f661e93e4ffbae6788;p=dbl.git users: Add a simple system to access users and groups Signed-off-by: Michael Tremer --- diff --git a/Makefile.am b/Makefile.am index 98adb9d..4ffe659 100644 --- a/Makefile.am +++ b/Makefile.am @@ -63,6 +63,7 @@ dist_pkgpython_PYTHON = \ src/dnsbl/logger.py \ src/dnsbl/reports.py \ src/dnsbl/sources.py \ + src/dnsbl/users.py \ src/dnsbl/util.py dist_pkgpython_api_PYTHON = \ diff --git a/configure.ac b/configure.ac index a6db886..d255af0 100644 --- a/configure.ac +++ b/configure.ac @@ -56,6 +56,7 @@ AX_PYTHON_MODULE([dns], [fatal]) AX_PYTHON_MODULE([babel], [fatal]) AX_PYTHON_MODULE([fastapi], [fatal]) AX_PYTHON_MODULE([httpx], [fatal]) +AX_PYTHON_MODULE([ldap], [fatal]) AX_PYTHON_MODULE([publicsuffix2], [fatal]) AX_PYTHON_MODULE([rich], [fatal]) AX_PYTHON_MODULE([sqlmodel], [fatal]) diff --git a/src/dnsbl/__init__.py b/src/dnsbl/__init__.py index bed4806..7554d40 100644 --- a/src/dnsbl/__init__.py +++ b/src/dnsbl/__init__.py @@ -39,6 +39,7 @@ from . import domains from . import lists from . import reports from . import sources +from . import users class Backend(object): def __init__(self, config=None, debug=False): @@ -113,6 +114,10 @@ class Backend(object): def sources(self): return sources.Sources(self) + @functools.cached_property + def users(self): + return users.Users(self) + def search(self, name): """ Searches for a domain diff --git a/src/dnsbl/users.py b/src/dnsbl/users.py new file mode 100644 index 0000000..7752b04 --- /dev/null +++ b/src/dnsbl/users.py @@ -0,0 +1,224 @@ +############################################################################### +# # +# dnsbl - A DNS Blocklist Compositor For IPFire # +# Copyright (C) 2026 IPFire Development Team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + +import email.message +import email.utils +import ldap +import logging +import subprocess + +# Setup logging +log = logging.getLogger(__name__) + +class Users(object): + def __init__(self, backend): + self.backend = backend + + def connect(self): + """ + Creates a new LDAP connection + """ + uri = self.backend.config.get("ldap", "uri", fallback=None) + + # Fail if we have no LDAP URI configured + if not uri: + raise RuntimeError("LDAP URI is not configured") + + # Log action + log.debug("Connecting to LDAP %s" % uri) + + # Connect to LDAP + return ldap.initialize(uri) + + def _query(self, filter, base=None, attrlist=None, **kwargs): + """ + Performs a query against the LDAP database + """ + # Connect to LDAP + conn = self.connect() + + # Fetch the base + if base is None: + base = self.backend.config.get("ldap", "base", fallback=None) + if not base: + raise RuntimeError("LDAP base is not configured") + + log.debug("Performing LDAP query: %s" % filter) + + # Send the query + id = conn.search_ext(base, ldap.SCOPE_SUBTREE, filter, + attrlist=attrlist, **kwargs) + + # Fetch the result + type, result = conn.result(id) + + return result + + def get_by_uid(self, uid): + """ + Fetches a user by its UID + """ + result = self._query( + "(&(objectClass=person)(uid=%s))" % uid, sizelimit=1, + ) + + # Return the user + for dn, attrs in result: + return User(self.backend, dn, attrs) + + def get_by_dn(self, dn): + """ + Fetches a user by its DN + """ + result = self._query("(objectClass=*)", base=dn, sizelimit=1) + + # Return the user + for dn, attrs in result: + return User(self.backend, dn, attrs) + + def get_group(self, name): + """ + Fetches all users in a specific group + """ + result = self._query( + "(|" + "(&(objectClass=groupOfNames)(cn=%s))" + "(&(objectClass=posixGroup)(cn=%s))" + ")" % (name, name), sizelimit=1, + ) + + # Return the group + for dn, attrs in result: + return Group(self.backend, dn, attrs) + + # Admins + + @property + def admins(self): + return self.get_group("dnsbl-admins") + + # Moderators + + @property + def moderators(self): + return self.get_group("dnsbl-moderators") + + +class LDAPObject(object): + def __init__(self, backend, dn, attrs): + self.backend, self.dn, self.attrs = backend, dn, attrs + + def _get(self, key): + for value in self.attrs.get(key, []): + yield value + + def _get_strings(self, key): + for value in self._get(key): + yield value.decode() + + def _get_string(self, key, default=None): + for value in self._get_strings(key): + return value + + return default + + +class User(LDAPObject): + def __str__(self): + return self.cn + + @property + def uid(self): + """ + UID + """ + return self._get_string("uid") + + @property + def cn(self): + """ + Common Name + """ + return self._get_string("cn") + + @property + def mail(self): + """ + Mail + """ + return self._get_string("mail") + + def sendmail(self, message, headers=None): + """ + Sends the given message to this user + """ + if not isinstance(message, email.message.EmailMessage): + content = message + + # Create a new message object + message = email.message.EmailMessage() + message.set_content(content) + + # Set the recipient + message["To"] = email.utils.formataddr((self.cn, self.mail)) + + # Set headers + if headers: + for header in headers: + message[header] = headers[header] + + # Set a sender if none set + if not "From" in message: + message["From"] = email.utils.formataddr(("IPFire DNSBL", "no-reply@ipfire.org")) + + # Log the email + log.debug("Sending email:\n%s" % message.as_string()) + + # Launch sendmail + sendmail = subprocess.Popen( + ["/usr/sbin/sendmail", "-t", "-oi"], stdin=subprocess.PIPE, text=True, + ) + + # Pipe the email into sendmail + sendmail.communicate(message.as_string()) + + +class Group(LDAPObject): + def __str__(self): + return self.cn + + def __iter__(self): + """ + Returns all users in this group + """ + # Fetch all users by their DN + for dn in self._get_strings("member"): + yield self.backend.users.get_by_dn(dn) + + # Fetch all users by their UID + for uid in self._get_strings("memberUid"): + yield self.backend.users.get_by_uid(uid) + + @property + def cn(self): + """ + Common Name + """ + return self._get_string("cn")