from twisted.internet import defer, error
from twisted.python.failure import Failure
from zope.interface import implementer
from ldaptor import interfaces, entry, entryhelpers
from ldaptor.protocols.ldap import distinguishedname, ldaperrors, ldifprotocol
[docs]class LDAPCannotRemoveRootError(ldaperrors.LDAPNamingViolation):
"""Cannot remove root of LDAP tree"""
[docs]@implementer(interfaces.IConnectedLDAPEntry)
class ReadOnlyInMemoryLDAPEntry(
entry.EditableLDAPEntry,
entryhelpers.DiffTreeMixin,
entryhelpers.SubtreeFromChildrenMixin,
entryhelpers.MatchMixin,
entryhelpers.SearchByTreeWalkingMixin,
):
def __init__(self, *a, **kw):
entry.BaseLDAPEntry.__init__(self, *a, **kw)
self._parent = None
self._children = {}
[docs] def parent(self):
return self._parent
[docs] def children(self, callback=None):
if callback is None:
return defer.succeed(list(self._children.values()))
else:
for c in self._children.values():
callback(c)
return defer.succeed(None)
def _lookup(self, dn):
if not self.dn.contains(dn):
raise ldaperrors.LDAPNoSuchObject(dn.getText())
if dn == self.dn:
return defer.succeed(self)
for c in self._children.values():
if c.dn.contains(dn):
return c.lookup(dn)
raise ldaperrors.LDAPNoSuchObject(dn.getText())
[docs] def lookup(self, dn):
if not isinstance(dn, distinguishedname.DistinguishedName):
dn = distinguishedname.DistinguishedName(stringValue=dn)
return defer.maybeDeferred(self._lookup, dn)
[docs] def fetch(self, *attributes):
return defer.succeed(self)
[docs] def addChild(self, rdn, attributes):
"""TODO ugly API. Returns the created entry."""
rdn = distinguishedname.RelativeDistinguishedName(rdn)
rdn_str = rdn.getText()
if rdn_str in self._children:
raise ldaperrors.LDAPEntryAlreadyExists(
self._children[rdn_str].dn.getText()
)
dn = distinguishedname.DistinguishedName(listOfRDNs=(rdn,) + self.dn.split())
e = self.__class__(dn, attributes)
e._parent = self
self._children[rdn_str] = e
return e
def _delete(self):
if self._parent is None:
raise LDAPCannotRemoveRootError()
if self._children:
raise ldaperrors.LDAPNotAllowedOnNonLeaf(self.dn)
return self._parent.deleteChild(self.dn.split()[0])
[docs] def delete(self):
return defer.maybeDeferred(self._delete)
def _deleteChild(self, rdn):
if not isinstance(rdn, distinguishedname.RelativeDistinguishedName):
rdn = distinguishedname.RelativeDistinguishedName(stringValue=rdn)
rdn_str = rdn.getText()
try:
return self._children.pop(rdn_str)
except KeyError:
raise ldaperrors.LDAPNoSuchObject(rdn.getText())
[docs] def deleteChild(self, rdn):
return defer.maybeDeferred(self._deleteChild, rdn)
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
root = self
while root._parent is not None:
root = root._parent
d = defer.maybeDeferred(root.lookup, newDN.up())
else:
d = defer.succeed(None)
d.addCallback(self._move2, newDN)
return d
def _move2(self, newParent, newDN):
if newParent is not None:
newParent._children[newDN.split()[0].getText()] = self
del self._parent._children[self.dn.split()[0].getText()]
# 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)
self.dn = newDN
return self
[docs] def move(self, newDN):
return defer.maybeDeferred(self._move, newDN)
[docs] def commit(self):
return defer.succeed(True)
[docs]class InMemoryLDIFProtocol(ldifprotocol.LDIF):
"""
Receive LDIF data and gather results into an ReadOnlyInMemoryLDAPEntry.
You can override lookupFailed and addFailed to provide smarter
error handling. They are called as Deferred errbacks; returning
the reason causes error to pass onward and abort the whole
operation. Returning None from lookupFailed skips that entry, but
continues loading.
When the full LDIF data has been read, the completed Deferred will
trigger.
"""
def __init__(self):
# Do not access this via db, just to make sure you respect the ordering
self.db = None
self._deferred = defer.Deferred()
self.completed = defer.Deferred()
def _addEntry(self, db, entry):
d = db.lookup(entry.dn.up())
d.addErrback(self.lookupFailed, entry)
def _add(parent, entry):
if parent is not None:
parent.addChild(rdn=entry.dn.split()[0], attributes=entry)
d.addCallback(_add, entry)
d.addErrback(self.addFailed, entry)
def _passDB(_, db):
return db
d.addCallback(_passDB, db)
return d
[docs] def gotEntry(self, entry):
if self.db is None:
# first entry, create the db, prepare to process the rest
self.db = ReadOnlyInMemoryLDAPEntry(dn=entry.dn, attributes=entry)
self._deferred.callback(self.db)
else:
self._deferred.addCallback(self._addEntry, entry)
[docs] def lookupFailed(self, reason, entry):
return reason # pass the error (abort) by default
[docs] def addFailed(self, reason, entry):
return reason # pass the error (abort) by default
[docs] def connectionLost(self, reason):
super().connectionLost(reason)
if not reason.check(error.ConnectionDone):
self._deferred.addCallback(lambda db: reason)
else:
self._deferred.chainDeferred(self.completed)
del self._deferred # invalidate it to flush out bugs
[docs]def fromLDIFFile(f):
"""Read LDIF data from a file."""
p = InMemoryLDIFProtocol()
while 1:
data = f.read()
if not data:
break
p.dataReceived(data)
p.connectionLost(Failure(error.ConnectionDone()))
return p.completed