Source code for ldaptor.ldiftree

"""
Manage LDAP data as a tree of LDIF files.
"""
import os, errno
from zope.interface import implements
from twisted.internet import defer, error
from twisted.python import failure
from ldaptor import entry, interfaces, attributeset, entryhelpers
from ldaptor.protocols.ldap import ldifprotocol, distinguishedname, ldaperrors
from twisted.mail.maildir import _generateMaildirName as tempName

[docs]class LDIFTreeEntryContainsMultipleEntries(Exception): """LDIFTree entry contains multiple LDIF entries."""
[docs]class LDIFTreeEntryContainsNoEntries(Exception): """LDIFTree entry does not contain a valid LDIF entry."""
[docs]class LDIFTreeNoSuchObject(Exception): # TODO combine with standard LDAP errors """LDIFTree does not contain such entry."""
[docs]class LDAPCannotRemoveRootError(ldaperrors.LDAPNamingViolation): """Cannot remove root of LDAP tree"""
# TODO share with ldaptor.inmemory?
[docs]class StoreParsedLDIF(ldifprotocol.LDIF): def __init__(self): self.done = False self.seen = []
[docs] def gotEntry(self, obj): self.seen.append(obj)
[docs] def connectionLost(self, reason): self.done = True
[docs]def get(path, dn): return defer.maybeDeferred(_get, path, dn)
def _get(path, dn): dn = distinguishedname.DistinguishedName(dn) l = list(dn.split()) assert len(l) >= 1 l.reverse() parser = StoreParsedLDIF() entry = os.path.join(path, *['%s.dir'%rdn for rdn in l[:-1]]) entry = os.path.join(entry, '%s.ldif'%l[-1]) f = file(entry) while 1: data = f.read(8192) if not data: break parser.dataReceived(data) parser.connectionLost(failure.Failure(error.ConnectionDone)) assert parser.done entries = parser.seen if len(entries) == 0: raise LDIFTreeEntryContainsNoEntries elif len(entries) > 1: raise LDIFTreeEntryContainsMultipleEntries, entries else: return entries[0] def _putEntry(fileName, entry): """fileName is without extension.""" tmp = fileName + '.' + tempName() + '.tmp' f = file(tmp, 'w') f.write(str(entry)) f.close() os.rename(tmp, fileName+'.ldif') # TODO atomicity def _put(path, entry): l = list(entry.dn.split()) assert len(l) >= 1 l.reverse() entryRDN = l.pop() if l: grandParent = os.path.join(path, *['%s.dir'%rdn for rdn in l[:-1]]) parentEntry = os.path.join(grandParent, '%s.ldif' % l[-1]) parentDir = os.path.join(grandParent, '%s.dir' % l[-1]) if not os.path.exists(parentDir): if not os.path.exists(parentEntry): raise LDIFTreeNoSuchObject, entry.dn.up() try: os.mkdir(parentDir) except OSError, e: if e.errno == errno.EEXIST: # we lost a race to create the directory, safe to ignore pass else: raise else: parentDir = path return _putEntry(os.path.join(parentDir, '%s'%entryRDN), entry)
[docs]def put(path, entry): return defer.execute(_put, path, entry)
[docs]class LDIFTreeEntry(entry.EditableLDAPEntry, entryhelpers.DiffTreeMixin, entryhelpers.SubtreeFromChildrenMixin, entryhelpers.MatchMixin, entryhelpers.SearchByTreeWalkingMixin, ): implements(interfaces.IConnectedLDAPEntry) def __init__(self, path, dn=None, *a, **kw): if dn is None: dn = '' entry.BaseLDAPEntry.__init__(self, dn, *a, **kw) self.path = path if dn != '': #TODO DistinguishedName.__nonzero__ self._load() def _load(self): assert self.path.endswith('.dir') entryPath = '%s.ldif' % self.path[:-len('.dir')] parser = StoreParsedLDIF() try: f = file(entryPath) except IOError, e: if e.errno == errno.ENOENT: return else: raise while 1: data = f.read(8192) if not data: break parser.dataReceived(data) parser.connectionLost(failure.Failure(error.ConnectionDone)) assert parser.done entries = parser.seen if len(entries) == 0: raise LDIFTreeEntryContainsNoEntries elif len(entries) > 1: raise LDIFTreeEntryContainsMultipleEntries, entries else: # TODO ugliness and all of its friends for k,v in entries[0].items(): self._attributes[k] = attributeset.LDAPAttributeSet(k, v)
[docs] def parent(self): # TODO add __nonzero__ to DistinguishedName if self.dn == '': # root return None else: parentPath, _ = os.path.split(self.path) return self.__class__(parentPath, self.dn.up())
def _sync_children(self): children = [] try: filenames = os.listdir(self.path) except OSError, e: if e.errno == errno.ENOENT: pass else: raise else: seen = set() for fn in filenames: base, ext = os.path.splitext(fn) if ext not in ['.dir', '.ldif']: continue if base in seen: continue seen.add(base) dn = distinguishedname.DistinguishedName( listOfRDNs=((distinguishedname.RelativeDistinguishedName(base),) + self.dn.split())) e = self.__class__(os.path.join(self.path, base + '.dir'), dn) children.append(e) return children def _children(self, callback=None): children = self._sync_children() if callback is None: return children else: for c in children: callback(c) return None
[docs] def children(self, callback=None): return defer.maybeDeferred(self._children, callback=callback)
[docs] def lookup(self, dn): dn = distinguishedname.DistinguishedName(dn) if not self.dn.contains(dn): return defer.fail(ldaperrors.LDAPNoSuchObject(dn)) if dn == self.dn: return defer.succeed(self) it = dn.split() me = self.dn.split() assert len(it) > len(me) assert ((len(me)==0) or (it[-len(me):] == me)) rdn = it[-len(me)-1] path = os.path.join(self.path, '%s.dir' % rdn) entry = os.path.join(self.path, '%s.ldif' % rdn) if not os.path.isdir(path) and not os.path.isfile(entry): return defer.fail(ldaperrors.LDAPNoSuchObject(dn)) else: childDN = distinguishedname.DistinguishedName(listOfRDNs=(rdn,)+me) c = self.__class__(path, childDN) return c.lookup(dn)
def _addChild(self, rdn, attributes): rdn = distinguishedname.RelativeDistinguishedName(rdn) for c in self._sync_children(): if c.dn.split()[0] == rdn: raise ldaperrors.LDAPEntryAlreadyExists, c.dn dn = distinguishedname.DistinguishedName(listOfRDNs= (rdn,) +self.dn.split()) e = entry.BaseLDAPEntry(dn, attributes) if not os.path.exists(self.path): os.mkdir(self.path) fileName = os.path.join(self.path, '%s' % rdn) tmp = fileName + '.' + tempName() + '.tmp' f = file(tmp, 'w') f.write(str(e)) f.close() os.rename(tmp, fileName+'.ldif') # TODO atomicity dirName = os.path.join(self.path, '%s.dir' % rdn) e = self.__class__(dirName, dn) return e
[docs] def addChild(self, rdn, attributes): d = self._addChild(rdn, attributes) return d
def _delete(self): if self.dn == '': ##TODO DistinguishedName __nonzero__ raise LDAPCannotRemoveRootError if self._sync_children(): raise ldaperrors.LDAPNotAllowedOnNonLeaf( 'Cannot remove entry with children: %s' % self.dn) assert self.path.endswith('.dir') entryPath = '%s.ldif' % self.path[:-len('.dir')] os.remove(entryPath) return self
[docs] def delete(self): return defer.maybeDeferred(self._delete)
def _deleteChild(self, rdn): if not isinstance(rdn, distinguishedname.RelativeDistinguishedName): rdn = distinguishedname.RelativeDistinguishedName(stringValue=rdn) for c in self._sync_children(): if c.dn.split()[0] == rdn: return c.delete() raise ldaperrors.LDAPNoSuchObject, rdn
[docs] def deleteChild(self, rdn): return defer.maybeDeferred(self._deleteChild, rdn)
def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.path, str(self.dn)) def __cmp__(self, other): if not isinstance(other, LDIFTreeEntry): return NotImplemented return cmp(self.dn, other.dn)
[docs] def commit(self): assert self.path.endswith('.dir') entryPath = self.path[:-len('.dir')] return defer.maybeDeferred(_putEntry, entryPath, self)
[docs] def move(self, newDN): return defer.maybeDeferred(self._move, newDN)
def _move(self, newDN): if not isinstance(newDN, distinguishedname.DistinguishedName): newDN = distinguishedname.DistinguishedName(stringValue=newDN) if newDN.up() != self.dn.up(): # climb up the tree to root rootDN = self.dn rootPath = self.path while rootDN != '': rootDN = rootDN.up() rootPath = os.path.dirname(rootPath) root = self.__class__(path=rootPath, dn=rootDN) d = defer.maybeDeferred(root.lookup, newDN.up()) else: d = defer.succeed(None) d.addCallback(self._move2, newDN) return d def _move2(self, newParent, newDN): # remove old RDN attributes for attr in self.dn.split()[0].split(): self[attr.attributeType].remove(attr.value) # add new RDN attributes for attr in newDN.split()[0].split(): # TODO what if the key does not exist? self[attr.attributeType].add(attr.value) newRDN = newDN.split()[0] srcdir = os.path.dirname(self.path) if newParent is None: dstdir = srcdir else: dstdir = newParent.path newpath = os.path.join(dstdir, '%s.dir' % newRDN) try: os.rename(self.path, newpath) except OSError, e: if e.errno == errno.ENOENT: pass else: raise basename, ext = os.path.splitext(self.path) assert ext == '.dir' os.rename('%s.ldif' % basename, os.path.join(dstdir, '%s.ldif' % newRDN)) self.dn = newDN self.path = newpath return self.commit()
if __name__ == '__main__': """ Demonstration LDAP server; serves an LDIFTree from given directory over LDAP on port 10389. """ from twisted.internet import reactor, protocol from twisted.python import log import sys log.startLogging(sys.stderr) from twisted.python import components from ldaptor.protocols.ldap import ldapserver path = sys.argv[1] db = LDIFTreeEntry(path) class LDAPServerFactory(protocol.ServerFactory): def __init__(self, root): self.root = root class MyLDAPServer(ldapserver.LDAPServer): debug = True components.registerAdapter(lambda x: x.root, LDAPServerFactory, interfaces.IConnectedLDAPEntry) factory = LDAPServerFactory(db) factory.protocol = MyLDAPServer reactor.listenTCP(10389, factory) reactor.run()