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 )