]> git.ipfire.org Git - dbl.git/commitdiff
users: Add a simple system to access users and groups
authorMichael Tremer <michael.tremer@ipfire.org>
Fri, 9 Jan 2026 15:57:40 +0000 (15:57 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Fri, 9 Jan 2026 15:57:40 +0000 (15:57 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
configure.ac
src/dnsbl/__init__.py
src/dnsbl/users.py [new file with mode: 0644]

index 98adb9d0b97e87fb9d9e1a9176a62a8195267adf..4ffe659f6d0518e93f03763ff4f5a5059e0ebc34 100644 (file)
@@ -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 = \
index a6db88602e52ddbd4d5a6a250a0846f4446a0a0e..d255af09bf8cc60597a8afce689d9c03017316a7 100644 (file)
@@ -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])
index bed4806bd8d36b69d32fd4f70f252bf74c974749..7554d4074d40bcc8fe863191adb39fa718d6b76c 100644 (file)
@@ -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 (file)
index 0000000..7752b04
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+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")