| Author: | Tommi Virtanen |
|---|
(presented on 2004-06-09)
dn: gn=John+sn=Doe,ou=Research & Development,ou=Pe
ople,dc=example,dc=com
objectClass: addressbookPerson
gn: John
sn: Doe
street: Back alley
postOfficeBox: 123
postalCode: 54321
postalAddress: Backstreet
st: NY
l: New York City
c: US
dn: gn=John+sn=Smith,ou=Marketing,ou=People,
dc=example,dc=com
objectClass: addressbookPerson
gn: John
sn: Smith
telephoneNumber: 555-1234
facsimileTelephoneNumber: 555-1235
description: This is a description that can span multi
ple lines as long as the non-first lines are inden
ted in the LDIF.
...
Batteries included!
Python combines remarkable power with very clear syntax.
Runs on many brands of UNIX, on Windows, OS/2, Mac, Amiga, and many other platforms.
>>> from ldaptor.protocols.ldap import distinguishedname
>>> dn=distinguishedname.DistinguishedName(
... 'dc=example,dc=com')
>>> dn
DistinguishedName(listOfRDNs=(RelativeDistinguishedName(
attributeTypesAndValues=(LDAPAttributeTypeAndValue(
attributeType='dc', value='example'),)),
RelativeDistinguishedName(attributeTypesAndValues=(
LDAPAttributeTypeAndValue(attributeType='dc', value='com'),))))
>>> str(dn)
'dc=example,dc=com'
Ldaptor is a set of pure-Python LDAP client programs, applications and a programming library.
It is licensed under the GNU LGPL.
>>> from ldaptor.protocols.ldap import \
... ldapclient, ldapconnector
>>> from twisted.internet import reactor
>>> connector=ldapconnector.LDAPClientCreator(reactor,
... ldapclient.LDAPClient)
>>> connector
<ldaptor.protocols.ldap.ldapconnector.LDAPClientCreator
instance at 0x40619b6c>
Twisted is an event-driven networking framework written in Python and licensed under the LGPL.
Twisted supports TCP, UDP, SSL/TLS, multicast, Unix sockets, a large number of protocols (including HTTP, NNTP, SSH, IRC, FTP, and others), and much more.
Twisted includes many fullblown applications, such as web, SSH, FTP, DNS and news servers.
>>> d=connector.connectAnonymously(dn,
... {dn: ('localhost', 10389)})
>>> d
<Deferred at 0x402d058c>
>>> from twisted.trial.util import deferredResult
>>> proto=deferredResult(d)
>>> proto
<ldaptor.protocols.ldap.ldapclient.LDAPClient
instance at 0x40619dac>
>>> from ldaptor.protocols.ldap import ldapsyntax
>>> baseEntry=ldapsyntax.LDAPEntry(client=proto, dn=dn)
>>> d2=baseEntry.search(filterText='(gn=j*)')
>>> results=deferredResult(d2)
>>> results
[LDAPEntry(dn='givenName=John+sn=Smith,ou=People,
dc=example,dc=com', attributes={'description': ['Some text.'],
'facsimileTelephoneNumber': ['555-1235'], 'givenName': ['John'],
'objectClass': ['addressbookPerson'], 'sn': ['Smith'],
'telephoneNumber': ['555-1234']}), LDAPEntry(dn=
'givenName=John+sn=Doe,ou=People,dc=example,dc=com',
attributes={'c': ['US'], 'givenName': ['John'], 'l': ['New York City'],
'objectClass': ['addressbookPerson'], 'postOfficeBox': ['123'],
'postalAddress': ['Backstreet'], 'postalCode': ['54321'],
'sn': ['Doe'], 'st': ['NY'], 'street': ['Back alley']})]
>>> results[0]
LDAPEntry(dn=
'givenName=John+sn=Smith,ou=People,dc=example,dc=com',
attributes={'description': ['Some text.'],
'facsimileTelephoneNumber': ['555-1235'], 'givenName': ['John'],
'objectClass': ['addressbookPerson'], 'sn': ['Smith'],
'telephoneNumber': ['555-1234']})
>>> results[3]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
IndexError: list index out of range
>>> print results[0]
dn: givenName=John+sn=Smith,ou=People,dc=example,dc=com
objectClass: addressbookPerson
description: Some text.
facsimileTelephoneNumber: 555-1235
givenName: John
sn: Smith
telephoneNumber: 555-1234
>>> proto.unbind()
>>> smith=results[0]
>>> print smith.dn
givenName=John+sn=Smith,ou=People,dc=example,dc=com
>>> smith['givenName']
['John']
>>>
A lot of similarities with OO programming languages, but some big differences, too.
An LDAP entry corresponds with an object.
Whereas object are usually instances of a single class, LDAP entries can "implement" multiple objectClasses.
objectClasses can inherit zero, one or many objectClasses, just like programming classes.
objectClasses have a root class, known as top; many object oriented programming languages have a root class, e.g. named Object.
objectClasses are either STRUCTURAL or AUXILIARY; entries can only implement one STRUCTURAL objectClass.
The objectClasses of an entry can be changed at will; you only need to take care that the entry has all the MUST attribute types, and no attribute types outside of the ones that are MUST or MAY.
Note that e.g. OpenLDAP doesn't implement this.
Attributes of an entry closely match attributes of objects in programming languages; however, LDAP attributes may have multiple values.
An example search filter:
(cn=John Smith)
#!/usr/bin/python
from twisted.internet import reactor, defer
from ldaptor.protocols.ldap import ldapclient, ldapsyntax, ldapconnector, \
distinguishedname
from ldaptor import ldapfilter
def search(config):
c=ldapconnector.LDAPClientCreator(reactor, ldapclient.LDAPClient)
d=c.connectAnonymously(config['base'],
config['serviceLocationOverrides'])
def _doSearch(proto, config):
searchFilter = ldapfilter.parseFilter('(gn=j*)')
baseEntry = ldapsyntax.LDAPEntry(client=proto, dn=config['base'])
d=baseEntry.search(filterObject=searchFilter)
return d
d.addCallback(_doSearch, config)
return d
def main():
import sys
from twisted.python import log
log.startLogging(sys.stderr, setStdout=0)
config = {
'base':
distinguishedname.DistinguishedName('ou=People,dc=example,dc=com'),
'serviceLocationOverrides': {
distinguishedname.DistinguishedName('dc=example,dc=com'):
('localhost', 10389),
}
}
d = search(config)
def _show(results):
for item in results:
print item
d.addCallback(_show)
d.addErrback(defer.logError)
d.addBoth(lambda _: reactor.stop())
reactor.run()
if __name__ == '__main__':
main()
http://www.divmod.org/Home/Projects/Nevow/
import os
from twisted.internet import reactor
from nevow import rend, appserver, inevow, compy, \
stan, loaders
from formless import annotate, webform, iformless
from ldaptor.protocols.ldap import ldapclient, ldapsyntax, ldapconnector, \
distinguishedname
from ldaptor import ldapfilter
from ldaptor.protocols import pureldap
class ILDAPConfig(compy.Interface):
"""Addressbook configuration retrieval."""
def getBaseDN(self):
"""Get the LDAP base DN, as a DistinguishedName."""
def getServiceLocationOverrides(self):
"""
Get the LDAP service location overrides, as a mapping of
DistinguishedName to (host, port) tuples.
"""
class LDAPConfig(object):
__implements__ = ILDAPConfig
def __init__(self,
baseDN,
serviceLocationOverrides=None):
self.baseDN = distinguishedname.DistinguishedName(baseDN)
self.serviceLocationOverrides = {}
if serviceLocationOverrides is not None:
for k,v in serviceLocationOverrides.items():
dn = distinguishedname.DistinguishedName(k)
self.serviceLocationOverrides[dn]=v
def getBaseDN(self):
return self.baseDN
def getServiceLocationOverrides(self):
return self.serviceLocationOverrides
class IAddressBookSearch(annotate.TypedInterface):
def search(self,
sn = annotate.String(label="Last name"),
givenName = annotate.String(label="First name"),
telephoneNumber = annotate.String(),
description = annotate.String()):
pass
search = annotate.autocallable(search)
class CurrentSearch(object):
__implements__ = IAddressBookSearch, inevow.IContainer
data = {}
def _getSearchFilter(self):
filters = []
for attr,value in self.data.items():
if value is not None:
f = ldapfilter.parseMaybeSubstring(attr, value)
filters.append(f)
if not filters:
return None
searchFilter = pureldap.LDAPFilter_and(
[pureldap.LDAPFilter_equalityMatch(
attributeDesc=pureldap.LDAPAttributeDescription('objectClass'),
assertionValue=pureldap.LDAPAssertionValue('addressbookPerson'))]
+ filters)
return searchFilter
def search(self, **kw):
for k,v in kw.items():
if v is None:
del kw[k]
self.data = kw
return self
def __nonzero__(self):
return bool(self.data)
def __iter__(self):
if self.data is None:
return
for k,v in self.data.items():
yield (k,v)
def child(self, context, name):
if name == 'searchFilter':
return self._getSearchFilter()
if name != 'results':
return None
config = context.locate(ILDAPConfig)
c=ldapconnector.LDAPClientCreator(reactor, ldapclient.LDAPClient)
d=c.connectAnonymously(config.getBaseDN(),
config.getServiceLocationOverrides())
def _search(proto, base, searchFilter):
baseEntry = ldapsyntax.LDAPEntry(client=proto, dn=base)
d=baseEntry.search(filterObject=searchFilter)
return d
d.addCallback(_search, config.getBaseDN(), self._getSearchFilter())
return d
def LDAPFilterSerializer(original, context):
return original.asText()
# TODO need to make this pretty some day.
for c in [
pureldap.LDAPFilter_and,
pureldap.LDAPFilter_or,
pureldap.LDAPFilter_not,
pureldap.LDAPFilter_substrings,
pureldap.LDAPFilter_equalityMatch,
pureldap.LDAPFilter_greaterOrEqual,
pureldap.LDAPFilter_lessOrEqual,
pureldap.LDAPFilter_approxMatch,
pureldap.LDAPFilter_present,
pureldap.LDAPFilter_extensibleMatch,
]:
compy.registerAdapter(LDAPFilterSerializer,
c,
inevow.ISerializable)
class AddressBookResource(rend.Page):
docFactory = loaders.xmlfile(
'searchform.xhtml',
templateDir=os.path.split(os.path.abspath(__file__))[0])
def configurable_(self, context):
try:
i = context.locate(inevow.IHand)
except KeyError:
i = CurrentSearch()
return i
def data_search(self, context, data):
configurable = self.locateConfigurable(context, '')
cur = configurable.original
return cur
def child_form_css(self, request):
return webform.defaultCSS
def render_input(self, context, data):
formDefaults = context.locate(iformless.IFormDefaults)
methodDefaults = formDefaults.getAllDefaults('search')
conf = self.configurable_(context)
for k,v in conf:
methodDefaults[k] = v
return webform.renderForms()
def render_haveSearch(self, context, data):
r=context.allPatterns(str(bool(data)))
return context.tag.clear()[r]
def render_searchFilter(self, context, data):
return data.asText()
def render_iterateMapping(self, context, data):
headers = context.allPatterns('header')
keyPattern = context.patternGenerator('key')
valuePattern = context.patternGenerator('value')
divider = context.patternGenerator('divider', default=stan.invisible)
content = [(keyPattern(data=key),
valuePattern(data=value),
divider())
for key, value in data.items()]
if not content:
content = context.allPatterns('empty')
else:
# No divider after the last thing.
content[-1] = content[-1][:-1]
footers = context.allPatterns('footer')
return context.tag.clear()[ headers, content, footers ]
def getSite(config):
form = AddressBookResource()
form.remember(config, ILDAPConfig)
site = appserver.NevowSite(form)
return site
<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:nevow="http://nevow.com/ns/nevow/0.1">
<head>
<title>Address book search</title>
<link href="/form_css" rel="stylesheet" />
</head>
<body>
<h1>Address book search</h1>
<form nevow:render="input" action="post">
<p nevow:render="Remove">Search form will be here.</p>
</form>
<div nevow:render="haveSearch" nevow:data="search">
<p nevow:pattern="False">No search given.</p>
<div nevow:pattern="True">
<ol nevow:render="sequence" nevow:data="results">
<p nevow:pattern="empty">No entries found.</p>
<li nevow:pattern="item" style="margin-bottom: 2em;">
<dl nevow:render="iterateMapping">
<dt nevow:pattern="key" nevow:render="string">
Attribute type goes here
</dt>
<nevow:invisible nevow:pattern="value" nevow:render="sequence">
<dd nevow:pattern="item" nevow:render="string">
Attribute value goes here
</dd>
</nevow:invisible>
</dl>
</li>
</ol>
<p>
Used search filter:
<span nevow:render="searchFilter" nevow:data="searchFilter"/>
</p>
</div>
</div>
</body>
</html>
# -*- python -*-
from twisted.application import service, internet
import addressbook
config = addressbook.LDAPConfig(
baseDN='ou=People,dc=example,dc=com',
serviceLocationOverrides={
'dc=example,dc=com': ('localhost', 10389),
})
application = service.Application("LDAPressBook")
site = addressbook.getSite(config)
webServer = internet.TCPServer(8088, site)
webServer.setServiceParent(application)
(&(objectClass=person)
(!(telephoneNumber=*))
(|(cn=*a*b*)(cn=*b*a*)))
attributetype ( 2.5.4.4 NAME ( 'sn' 'surname' )
DESC 'RFC2256: last (family) name(s) for
which the entity is known by'
SUP name )
Can also contain
objectclass ( 2.5.6.6 NAME 'person'
DESC 'RFC2256: a person'
SUP top STRUCTURAL
MUST ( sn $ cn )
MAY ( userPassword $ telephoneNumber
$ seeAlso $ description ) )
...
Install OpenLDAP.
Install Ldaptor, play around with ldaptor-webui.
Learn Python.
Learn Twisted. Write a client application for a simple protocol. Read the HOWTOs.
Questions?