From 28e7cf8e5b4621655a08f2158ee8f6049a6d402c Mon Sep 17 00:00:00 2001 From: Kumi Date: Thu, 14 Sep 2023 14:44:51 +0200 Subject: [PATCH] Start LDAP implementation --- core/management/commands/serveldap.py | 16 +++++ ldap/__init__.py | 0 ldap/server.py | 87 +++++++++++++++++++++++++++ requirements.txt | 1 + 4 files changed, 104 insertions(+) create mode 100644 core/management/commands/serveldap.py create mode 100644 ldap/__init__.py create mode 100644 ldap/server.py diff --git a/core/management/commands/serveldap.py b/core/management/commands/serveldap.py new file mode 100644 index 0000000..0c9e314 --- /dev/null +++ b/core/management/commands/serveldap.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand +from django.conf import settings + +from ldap.server import LDAPServer + +from twisted.internet.protocol import Factory +from twisted.internet import reactor + +class Command(BaseCommand): + help = 'Provides a simple LDAP server' + + def handle(self, *args, **kwargs): + factory = Factory() + factory.protocol = LDAPServer + reactor.listenTCP(10389, factory) + reactor.run() diff --git a/ldap/__init__.py b/ldap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ldap/server.py b/ldap/server.py new file mode 100644 index 0000000..260df85 --- /dev/null +++ b/ldap/server.py @@ -0,0 +1,87 @@ +from zope.interface import implementer +from twisted.internet import reactor, defer +from twisted.internet.protocol import Factory +from ldaptor.protocols.ldap import ldapserver, ldapsyntax, distinguishedname, ldaperrors +from ldaptor import interfaces, entry +from django.contrib.auth import get_user_model, authenticate +from django.core.exceptions import ObjectDoesNotExist + +User = get_user_model() + +@implementer(interfaces.IConnectedLDAPEntry) +class DjangoUserEntry: + def __init__(self, user): + self.dn = distinguishedname.DistinguishedName('cn={},dc=kumidc'.format(user.username)) + self.attributes = {'cn': [user.username,], 'email': [user.email,]} + + def search(self, filterObject, attributes=None): + # This ignores the filter and always returns the authenticated user + # This LDAP server is only meant to be used for authentication, not as a directory service + return defer.succeed([self]) + + def lookup(self): + return defer.succeed(self) + + def bind(self, password): + username = self.dn.split(',')[0].split('=')[1] + user = authenticate(username=username, password=password) + + if user is not None: + self.__init__(user) + return defer.succeed(self) + else: + return defer.fail() + +class LDAPServer(ldapserver.LDAPServer): + def handle_LDAP_BIND_REQUEST(self, request, controls, reply): + if request.dn: + user_dn = str(request.dn) + try: + username = user_dn.split(',')[0].split('=')[1] + user = User.objects.get(username=username) + e = DjangoUserEntry(user) + except ObjectDoesNotExist: + return defer.fail(ldaperrors.LDAPInvalidCredentials()) + + d = e.bind(request.auth) + d.addCallbacks(reply, reply.handle_LDAPError) + return d + + elif request.sasl: + if request.sasl.mechanism == "PLAIN": + authzid, authcid, password = request.sasl.credentials.split('\x00') + e = DjangoUserEntry(user) + d = defer.succeed(e) + d.addCallbacks(reply, reply.handle_LDAPError) + return d + else: + return defer.fail(ldaperrors.LDAPInvalidCredentials()) + else: + return defer.fail(ldaperrors.LDAPInvalidCredentials()) + + def handle_LDAP_SEARCH_REQUEST(self, request, reply): + if not self.boundUser: + return defer.fail(ldaperrors.LDAPUnwillingToPerform()) + + search_filter = request.filter + f = request.filter.asText() + + try: + user = User.objects.get(username=self.boundUser) + e = DjangoUserEntry(user) + except ObjectDoesNotExist: + return defer.fail(ldaperrors.LDAPNoSuchObject()) + + if search_filter.present("objectClass") and request.scope == ldapclient.SCOPE_BASE: + entries = [e] + else: + entries = [] + + reply(entries) + return defer.succeed(()) + + + + def handleUnknown(self, op): + # Ignore requests we don't support + return defer.succeed(None) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d61718b..f06eeca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ django-autosecretkey cryptography pysaml2 +ldaptor pyotp django-timezone-field django-phonenumber-field[phonenumbers]