LDAP Servers

An LDAP directory information tree (DIT) is a highly specialized database with entries arranged in a tree-like structure.

File-System LDAP DIT

A minimal LDAP DIT that stores entries in the local file system

Code

First, a module that defines our DIT entries– schema.py

 1COUNTRY = (
 2    "dc=fr",
 3    {
 4        "objectClass": ["dcObject", "country"],
 5        "dc": ["fr"],
 6        "description": ["French country 2 letters iso description"],
 7    },
 8)
 9COMPANY = (
10    "dc=example",
11    {
12        "objectClass": ["dcObject", "organization"],
13        "dc": ["example"],
14        "description": ["My organisation"],
15        "o": ["Example, Inc"],
16    },
17)
18PEOPLE = (
19    "ou=people",
20    {
21        "ou": ["people"],
22        "description": ["People from Example Inc"],
23        "objectclass": ["organizationalunit"],
24    },
25)
26USERS = [
27    (
28        "uid=yoen",
29        {
30            "objectClass": ["people", "inetOrgPerson"],
31            "cn": ["Yoen Van der Weld"],
32            "sn": ["Van der Weld"],
33            "givenName": ["Yoen"],
34            "uid": ["yoen"],
35            "mail": ["/home/yoen/mailDir"],
36            "userPassword": ["secret"],
37        },
38    ),
39    (
40        "uid=esteban",
41        {
42            "objectClass": ["people", "inetOrgPerson"],
43            "cn": ["Esteban Garcia Marquez"],
44            "sn": ["Garcia Marquez"],
45            "givenName": ["Esteban"],
46            "uid": ["esteban"],
47            "mail": ["/home/esteban/mailDir"],
48            "userPassword": ["secret2"],
49        },
50    ),
51    (
52        "uid=mohamed",
53        {
54            "objectClass": ["people", "inetOrgPerson"],
55            "cn": ["Mohamed Al Ghâlib"],
56            "sn": ["Al Ghâlib"],
57            "givenName": ["mohamed"],
58            "uid": ["mohamed"],
59            "mail": ["/home/mohamed/mailDir"],
60            "userPassword": ["secret3"],
61        },
62    ),
63]

Next, the server code– ldaptor_basic.py

 1#! /usr/bin/env python
 2
 3"""
 4Testing a simple ldaptor ldap server
 5Base on an example by Gaston TJEBBES aka "tonthon":
 6http://tonthon.blogspot.com/2011/02/ldaptor-ldap-with-twisted-server-side.html
 7"""
 8
 9import tempfile, sys
10
11from twisted.application import service
12from twisted.internet import reactor
13from twisted.internet.protocol import ServerFactory
14from twisted.python.components import registerAdapter
15from twisted.python import log
16from ldaptor.interfaces import IConnectedLDAPEntry
17from ldaptor.protocols.ldap.ldapserver import LDAPServer
18from ldaptor.ldiftree import LDIFTreeEntry
19
20from schema import COUNTRY, COMPANY, PEOPLE, USERS
21
22
23class Tree:
24    def __init__(self):
25        dirname = tempfile.mkdtemp(".ldap", "test-server", "/tmp")
26        self.db = LDIFTreeEntry(dirname)
27        self.init_db()
28
29    def init_db(self):
30        """
31        Add subtrees to the top entry
32        top->country->company->people
33        """
34        country = self.db.addChild(COUNTRY[0], COUNTRY[1])
35        company = country.addChild(COMPANY[0], COMPANY[1])
36        people = company.addChild(PEOPLE[0], PEOPLE[1])
37        for user in USERS:
38            people.addChild(user[0], user[1])
39
40
41class LDAPServerFactory(ServerFactory):
42    """
43    Our Factory is meant to persistently store the ldap tree
44    """
45
46    protocol = LDAPServer
47
48    def __init__(self, root):
49        self.root = root
50
51    def buildProtocol(self, addr):
52        proto = self.protocol()
53        proto.debug = self.debug
54        proto.factory = self
55        return proto
56
57
58if __name__ == "__main__":
59    if len(sys.argv) == 2:
60        port = int(sys.argv[1])
61    else:
62        port = 8080
63    # First of all, to show logging info in stdout :
64    log.startLogging(sys.stderr)
65    # We initialize our tree
66    tree = Tree()
67    # When the ldap protocol handle the ldap tree,
68    # it retrieves it from the factory adapting
69    # the factory to the IConnectedLDAPEntry interface
70    # So we need to register an adapter for our factory
71    # to match the IConnectedLDAPEntry
72    registerAdapter(lambda x: x.root, LDAPServerFactory, IConnectedLDAPEntry)
73    # Run it !!
74    factory = LDAPServerFactory(tree.db)
75    factory.debug = True
76    application = service.Application("ldaptor-server")
77    myService = service.IServiceCollection(application)
78    reactor.listenTCP(port, factory)
79    reactor.run()

LDAP Server which allows BIND with UPN

The LDAP server implemented by Microsoft Active Directory allows using the UPN as the BIND DN.

It is possible to implement something similar using ldaptor.

Below is a proof-of-concept implementation, which should not be used for production as it has an heuristic method for detecting which BIND DN is an UPN.

handle_LDAPBindRequest is the method called when a BIND request is received.

 1"""
 2An ldaptor LDAP server which can authenticate based on UPN, as AD does.
 3
 4The LDAP entry needs to have the ``userPrincipalName`` attribute set:
 5
 6    dn: uid=bob,ou=people,dc=example,dc=org
 7    objectclass: top
 8    objectclass: person
 9    objectClass: inetOrgPerson
10    uid: bob
11    cn: bobby
12    gn: Bob
13    sn: Roberts
14    mail: bob@example.org
15    homeDirectory: e:\\Users\\bob
16    userPassword: pass
17    userPrincipalName: bob@ad.example.org
18
19A UPN bind arrives as ``User.Name@ad.example.tld`` in the BIND DN slot
20rather than a normal distinguished name. This server intercepts the BIND
21request, looks up the entry whose ``userPrincipalName`` matches, and
22rewrites the request's DN to that entry's real DN before delegating to
23the stock :class:`LDAPServer` bind handler. Non-UPN BIND requests are
24forwarded unchanged.
25"""
26
27from ldaptor import interfaces
28from ldaptor.protocols import pureldap
29from twisted.internet import defer
30from ldaptor.protocols.ldap.ldapserver import LDAPServer
31
32
33class LDAPServerWithUPNBind(LDAPServer):
34    """
35    An LDAP server which supports BIND using a UPN (User Principal Name),
36    similar to Active Directory.
37    """
38
39    _loginAttribute = b"userPrincipalName"
40
41    @defer.inlineCallbacks
42    def handle_LDAPBindRequest(self, request, *args, **kwargs):
43        resolved = yield self._resolveUPNBindDN(request)
44        result = yield super().handle_LDAPBindRequest(resolved, *args, **kwargs)
45        return result
46
47    @defer.inlineCallbacks
48    def _resolveUPNBindDN(self, request):
49        """
50        If ``request`` looks like a UPN bind, resolve it to a real BIND DN.
51
52        A UPN takes the form ``User.Name@ad.example.tld``: it contains an
53        ``@`` but no ``,``, so it is not a valid distinguished name. When
54        the shape matches, search the directory for the entry whose
55        ``userPrincipalName`` matches and return a rewritten
56        :class:`LDAPBindRequest` targeting that entry's DN. Otherwise
57        return ``request`` unchanged so the caller can fall through to the
58        normal DN-based bind path.
59        """
60        if b"@" not in request.dn or b"," in request.dn:
61            # Not a UPN request; leave the DN alone.
62            return request
63
64        root = interfaces.IConnectedLDAPEntry(self.factory)
65        filter_text = b"(" + self._loginAttribute + b"=" + request.dn + b")"
66        results = yield root.search(filterText=filter_text)
67
68        if len(results) != 1:
69            # No unambiguous UPN match; fall through to the requested BIND
70            # DN and let the stock handler reject it as usual.
71            return request
72
73        return pureldap.LDAPBindRequest(
74            version=request.version,
75            dn=results[0].dn.getText(),
76            auth=request.auth,
77            tag=request.tag,
78            sasl=request.sasl,
79        )