Source code for ldaptor.protocols.ldap.ldapsyntax

"""Pythonic API for LDAP operations."""
import functools

from twisted.internet import defer
from twisted.python.failure import Failure
from zope.interface import implementer

from ldaptor.protocols.ldap import ldapclient, ldif, distinguishedname, ldaperrors
from ldaptor.protocols import pureldap, pureber
from ldaptor.samba import smbpassword
from ldaptor import ldapfilter, interfaces, delta, attributeset, entry
from ldaptor._encoder import to_bytes


[docs]class PasswordSetAggregateError(Exception): """Some of the password plugins failed""" def __init__(self, errors): Exception.__init__(self) self.errors = errors def __str__(self): return "{}: {}.".format( self.__doc__, "; ".join( [ f"{name} failed with {fail.getErrorMessage()}" for name, fail in self.errors ] ), ) def __repr__(self): return "<" + self.__class__.__name__ + " errors=" + repr(self.errors) + ">"
[docs]class PasswordSetAborted(Exception): """Aborted""" def __str__(self): return self.__doc__
[docs]class DNNotPresentError(Exception): """The requested DN cannot be found by the server."""
[docs]class ObjectInBadStateError(Exception): """The LDAP object in in a bad state."""
[docs]class ObjectDeletedError(ObjectInBadStateError): """The LDAP object has already been removed, unable to perform operations on it."""
[docs]class ObjectDirtyError(ObjectInBadStateError): """The LDAP object has a journal which needs to be committed or undone before this operation."""
[docs]class NoContainingNamingContext(Exception): """The server contains to LDAP naming context that would contain this object."""
[docs]class CannotRemoveRDNError(Exception): """The attribute to be removed is the RDN for the object and cannot be removed.""" def __init__(self, key, val=None): Exception.__init__(self) self.key = key self.val = val def __str__(self): if self.val is None: r = repr(self.key) else: r = f"{repr(self.key)}={repr(self.val)}" return ( """The attribute to be removed, %s, is the RDN for the object and cannot be removed.""" % r )
[docs]class MatchNotImplemented(NotImplementedError): """Match type not implemented""" def __init__(self, op): Exception.__init__(self) self.op = op def __str__(self): return f"{self.__doc__}: {self.op!r}"
[docs]class JournaledLDAPAttributeSet(attributeset.LDAPAttributeSet): def __init__(self, ldapObject, *a, **kw): self.ldapObject = ldapObject super().__init__(*a, **kw)
[docs] def add(self, value): self.ldapObject.journal(delta.Add(self.key, [value])) super().add(value)
[docs] def update(self, sequence): self.ldapObject.journal(delta.Add(self.key, sequence)) super().update(sequence)
[docs] def remove(self, value): if value not in self: raise LookupError(value) self.ldapObject._canRemove(self.key, value) self.ldapObject.journal(delta.Delete(self.key, [value])) super().remove(value)
[docs] def clear(self): self.ldapObject._canRemoveAll(self.key) super().clear() self.ldapObject.journal(delta.Delete(self.key))
[docs]@implementer( interfaces.ILDAPEntry, interfaces.IEditableLDAPEntry, interfaces.IConnectedLDAPEntry, ) class LDAPEntryWithClient(entry.EditableLDAPEntry): _state = "invalid" """ State of an LDAPEntry is one of: invalid - object not initialized yet ready - normal deleted - object has been deleted """ def __init__(self, client, dn, attributes={}, complete=0): """ Initialize the object. @param client: The LDAP client connection this object belongs to. @param dn: Distinguished Name of the object, as a string. @param attributes: Attributes of the object. A dictionary of attribute types to list of attribute values. """ super().__init__(dn, attributes) self.client = client self.complete = complete self._journal = [] self._remoteData = entry.EditableLDAPEntry(dn, attributes) self._state = "ready"
[docs] def buildAttributeSet(self, key, values): return JournaledLDAPAttributeSet(self, key, values)
def _canRemove(self, key, value): """ Called by JournaledLDAPAttributeSet when it is about to remove a value of an attributeType. """ self._checkState() for rdn in self.dn.split()[0].split(): if rdn.attributeType == key and rdn.value == value: raise CannotRemoveRDNError(key, value) def _canRemoveAll(self, key): """ Called by JournaledLDAPAttributeSet when it is about to remove all values of an attributeType. """ self._checkState() assert not isinstance(self.dn, str) for keyval in self.dn.split()[0].split(): if keyval.attributeType == key: raise CannotRemoveRDNError(key) def _checkState(self): if self._state != "ready": if self._state == "deleted": raise ObjectDeletedError else: raise ObjectInBadStateError( "State is {} while expecting {}".format( repr(self._state), repr("ready") ) )
[docs] def journal(self, journalOperation): """ Add a Modification into the list of modifications that need to be flushed to the LDAP server. Normal callers should not use this, they should use the o['foo']=['bar', 'baz'] -style API that enforces schema, handles errors and updates the cached data. """ self._journal.append(journalOperation)
# start ILDAPEntry def __getitem__(self, *a, **kw): self._checkState() return super().__getitem__(*a, **kw)
[docs] def get(self, *a, **kw): self._checkState() return super().get(*a, **kw)
[docs] def has_key(self, *a, **kw): self._checkState() return super().has_key(*a, **kw)
def __contains__(self, key): self._checkState() return self.has_key(key)
[docs] def keys(self): self._checkState() return super().keys()
[docs] def items(self): self._checkState() return super().items()
[docs] def toWire(self): a = [] objectClasses = list(self.get("objectClass", [])) objectClasses.sort() a.append(("objectClass", objectClasses)) lst = list(self.items()) lst.sort() for key, values in lst: if key != "objectClass": a.append((key, values)) return ldif.asLDIF(self.dn.getText(), a)
def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented if self.dn != other.dn: return False my = self.keys() my.sort() its = other.keys() its.sort() if my != its: return False for key in my: myAttr = self[key] itsAttr = other[key] if myAttr != itsAttr: return False return True def __ne__(self, other): return not self == other def __len__(self): return len(self.keys()) def __nonzero__(self): return True def __hash__(self): return hash(self.toWire())
[docs] def bind(self, password): r = pureldap.LDAPBindRequest(dn=self.dn.getText(), auth=password) d = self.client.send(r) d.addCallback(self._handle_bind_msg) return d
def _handle_bind_msg(self, msg): assert isinstance(msg, pureldap.LDAPBindResponse) assert msg.referral is None # TODO if msg.resultCode != ldaperrors.Success.resultCode: raise ldaperrors.get(msg.resultCode, msg.errorMessage) return self # end ILDAPEntry # start IEditableLDAPEntry def __setitem__(self, key, value): self._checkState() self._canRemoveAll(key) new = JournaledLDAPAttributeSet(self, key, value) super().__setitem__(key, new) self.journal(delta.Replace(key, value)) def __delitem__(self, key): self._checkState() self._canRemoveAll(key) super().__delitem__(key) self.journal(delta.Delete(key))
[docs] def undo(self): self._checkState() self._attributes.clear() for k, vs in self._remoteData.items(): self._attributes[k] = self.buildAttributeSet(k, vs) self._journal = []
def _assertMatchedDN(self, dn): assert dn == "" or dn == b"" def _commit_success(self, msg): assert isinstance(msg, pureldap.LDAPModifyResponse) assert msg.referral is None # TODO if msg.resultCode != ldaperrors.Success.resultCode: raise ldaperrors.get(msg.resultCode, msg.errorMessage) self._assertMatchedDN(msg.matchedDN) self._remoteData = entry.EditableLDAPEntry(self.dn, self) self._journal = [] return self
[docs] def commit(self): self._checkState() if not self._journal: return defer.succeed(self) op = pureldap.LDAPModifyRequest( object=self.dn.getText(), modification=[x.asLDAP() for x in self._journal] ) d = defer.maybeDeferred(self.client.send, op) d.addCallback(self._commit_success) return d
def _cbMoveDone(self, msg, newDN): assert isinstance(msg, pureldap.LDAPModifyDNResponse) assert msg.referral is None # TODO if msg.resultCode != ldaperrors.Success.resultCode: raise ldaperrors.get(msg.resultCode, msg.errorMessage) self._assertMatchedDN(msg.matchedDN) self.dn = newDN return self
[docs] def move(self, newDN): self._checkState() newDN = distinguishedname.DistinguishedName(newDN) newrdn = newDN.split()[0] newSuperior = distinguishedname.DistinguishedName(listOfRDNs=newDN.split()[1:]) newDN = distinguishedname.DistinguishedName((newrdn,) + newSuperior.split()) op = pureldap.LDAPModifyDNRequest( entry=self.dn.getText(), newrdn=newrdn.getText(), deleteoldrdn=1, newSuperior=newSuperior.getText(), ) d = self.client.send(op) d.addCallback(self._cbMoveDone, newDN) return d
def _cbDeleteDone(self, msg): assert isinstance(msg, pureldap.LDAPResult) if not isinstance(msg, pureldap.LDAPDelResponse): raise ldaperrors.get(msg.resultCode, msg.errorMessage) assert msg.referral is None # TODO if msg.resultCode != ldaperrors.Success.resultCode: raise ldaperrors.get(msg.resultCode, msg.errorMessage) self._assertMatchedDN(msg.matchedDN) return self
[docs] def delete(self): self._checkState() op = pureldap.LDAPDelRequest(entry=self.dn.getText()) d = self.client.send(op) d.addCallback(self._cbDeleteDone) self._state = "deleted" return d
def _cbAddDone(self, msg, dn): assert isinstance(msg, pureldap.LDAPAddResponse), ( "LDAPRequest response was not an LDAPAddResponse: %r" % msg ) assert msg.referral is None # TODO if msg.resultCode != ldaperrors.Success.resultCode: raise ldaperrors.get(msg.resultCode, msg.errorMessage) self._assertMatchedDN(msg.matchedDN) e = self.__class__(dn=dn, client=self.client) return e
[docs] def addChild(self, rdn, attributes): self._checkState() a = [] if attributes.get("objectClass", None): a.append(("objectClass", attributes["objectClass"])) del attributes["objectClass"] attributes = a + sorted(attributes.items()) del a rdn = distinguishedname.RelativeDistinguishedName(rdn) dn = distinguishedname.DistinguishedName(listOfRDNs=(rdn,) + self.dn.split()) ldapAttrs = [] for attrType, values in attributes: ldapAttrType = pureldap.LDAPAttributeDescription(attrType) lst = [] for value in values: if isinstance(value, str): value = value.encode("utf-8") lst.append(pureldap.LDAPAttributeValue(value)) ldapValues = pureber.BERSet(lst) ldapAttrs.append((ldapAttrType, ldapValues)) op = pureldap.LDAPAddRequest(entry=dn.getText(), attributes=ldapAttrs) d = self.client.send(op) d.addCallback(self._cbAddDone, dn) return d
def _cbSetPassword_ExtendedOperation(self, msg): assert isinstance(msg, pureldap.LDAPExtendedResponse) assert msg.referral is None # TODO if msg.resultCode != ldaperrors.Success.resultCode: raise ldaperrors.get(msg.resultCode, msg.errorMessage) self._assertMatchedDN(msg.matchedDN) return self
[docs] def setPassword_ExtendedOperation(self, newPasswd): """ Set the password on this object. @param newPasswd: A string containing the new password. @return: A Deferred that will complete when the operation is done. """ self._checkState() op = pureldap.LDAPPasswordModifyRequest( userIdentity=self.dn.getText(), newPasswd=newPasswd ) d = self.client.send(op) d.addCallback(self._cbSetPassword_ExtendedOperation) return d
_setPasswordPriority_ExtendedOperation = 0 setPasswordMaybe_ExtendedOperation = setPassword_ExtendedOperation
[docs] def setPassword_Samba(self, newPasswd, style=None): """ Set the Samba password on this object. @param newPasswd: A string containing the new password. @param style: one of 'sambaSamAccount', 'sambaAccount' or None. Specifies the style of samba accounts used. None is default and is the same as 'sambaSamAccount'. @return: A Deferred that will complete when the operation is done. """ self._checkState() nthash = smbpassword.nthash(newPasswd) lmhash = smbpassword.lmhash(newPasswd) if style is None: style = "sambaSamAccount" if style == "sambaSamAccount": self["sambaNTPassword"] = [nthash] self["sambaLMPassword"] = [lmhash] elif style == "sambaAccount": self["ntPassword"] = [nthash] self["lmPassword"] = [lmhash] else: raise RuntimeError("Unknown samba password style %r" % style) return self.commit()
_setPasswordPriority_Samba = 20
[docs] def setPasswordMaybe_Samba(self, newPasswd): """ Set the Samba password on this object if it is a sambaSamAccount or sambaAccount. @param newPasswd: A string containing the new password. @return: A Deferred that will complete when the operation is done. """ if not self.complete and not self.has_key("objectClass"): d = self.fetch("objectClass") d.addCallback( lambda dummy, self=self, newPasswd=newPasswd: self.setPasswordMaybe_Samba( newPasswd ) ) else: objectClasses = [to_bytes(s.upper()) for s in self.get("objectClass", ())] if b"SAMBAACCOUNT" in objectClasses: d = self.setPassword_Samba(newPasswd, style="sambaAccount") elif b"SAMBASAMACCOUNT" in objectClasses: d = self.setPassword_Samba(newPasswd, style="sambaSamAccount") else: d = defer.succeed(self) return d
def _cbSetPassword(self, dl, names): assert len(dl) == len(names) lst = [] for name, (ok, x) in zip(names, dl): if not ok: lst.append((name, x)) if lst: raise PasswordSetAggregateError(lst) return self def _cbSetPassword_one(self, result): return (True, None) def _ebSetPassword_one(self, fail): fail.trap(ldaperrors.LDAPException, DNNotPresentError) return (False, fail) def _setPasswordAll(self, results, newPasswd, prefix, names): if not names: return results name, names = names[0], names[1:] if results and not results[-1][0]: # failing fail = Failure(PasswordSetAborted()) d = defer.succeed(results + [(None, fail)]) else: fn = getattr(self, prefix + name) d = defer.maybeDeferred(fn, newPasswd) d.addCallbacks(self._cbSetPassword_one, self._ebSetPassword_one) def cb(result): (success, info) = result return results + [(success, info)] d.addCallback(cb) d.addCallback(self._setPasswordAll, newPasswd, prefix, names) return d
[docs] def setPassword(self, newPasswd): def _passwordChangerPriorityComparison(me, other): mePri = getattr(self, "_setPasswordPriority_" + me) otherPri = getattr(self, "_setPasswordPriority_" + other) return (mePri > otherPri) - (mePri < otherPri) prefix = "setPasswordMaybe_" names = [name[len(prefix) :] for name in dir(self) if name.startswith(prefix)] names.sort(key=functools.cmp_to_key(_passwordChangerPriorityComparison)) d = defer.maybeDeferred(self._setPasswordAll, [], newPasswd, prefix, names) d.addCallback(self._cbSetPassword, names) return d
# end IEditableLDAPEntry # start IConnectedLDAPEntry def _cbNamingContext_Entries(self, results): for result in results: for namingContext in result.get("namingContexts", ()): dn = distinguishedname.DistinguishedName(namingContext) if dn.contains(self.dn): return LDAPEntry(self.client, dn) raise NoContainingNamingContext(self.dn.getText())
[docs] def namingContext(self): o = LDAPEntry(client=self.client, dn="") d = o.search( filterText="(objectClass=*)", scope=pureldap.LDAP_SCOPE_baseObject, attributes=["namingContexts"], ) d.addCallback(self._cbNamingContext_Entries) return d
def _cbFetch(self, results, overWrite): if len(results) != 1: raise DNNotPresentError(self.dn.getText()) o = results[0] assert not self._journal if not overWrite: for key in list(self._remoteData.keys()): del self._remoteData[key] overWrite = o.keys() self.complete = 1 for k in overWrite: vs = o.get(k) if vs is not None: self._remoteData[k] = vs self.undo() return self
[docs] def fetch(self, *attributes): self._checkState() if self._journal: raise ObjectDirtyError( "cannot fetch attributes of %s, it is dirty" % repr(self) ) d = self.search(scope=pureldap.LDAP_SCOPE_baseObject, attributes=attributes) d.addCallback(self._cbFetch, overWrite=attributes) return d
def _cbSearchEntry(self, callback, objectName, attributes, complete): attrib = {} for key, values in attributes: attrib[to_bytes(key)] = [to_bytes(x) for x in values] o = LDAPEntry( client=self.client, dn=objectName, attributes=attrib, complete=complete ) callback(o) def _cbSearchMsg(self, msg, controls, d, callback, complete, sizeLimitIsNonFatal): if isinstance(msg, pureldap.LDAPSearchResultDone): assert msg.referral is None # TODO e = ldaperrors.get(msg.resultCode, msg.errorMessage) if not isinstance(e, ldaperrors.Success): try: raise e except ldaperrors.LDAPSizeLimitExceeded: if sizeLimitIsNonFatal: pass except Exception: d.errback(Failure()) return True # search ended successfully self._assertMatchedDN(msg.matchedDN) d.callback(controls) return True elif isinstance(msg, pureldap.LDAPSearchResultEntry): self._cbSearchEntry( callback, msg.objectName, msg.attributes, complete=complete ) return False elif isinstance(msg, pureldap.LDAPSearchResultReference): return False else: raise ldaperrors.LDAPProtocolError("bad search response: %r" % msg)
[docs] def search( self, filterText=None, filterObject=None, attributes=(), scope=None, derefAliases=None, sizeLimit=0, sizeLimitIsNonFatal=False, timeLimit=0, typesOnly=0, callback=None, controls=None, return_controls=False, ): self._checkState() d = defer.Deferred() if filterObject is None and filterText is None: filterObject = pureldap.LDAPFilterMatchAll elif filterObject is None and filterText is not None: filterObject = ldapfilter.parseFilter(filterText) elif filterObject is not None and filterText is None: pass elif filterObject is not None and filterText is not None: f = ldapfilter.parseFilter(filterText) filterObject = pureldap.LDAPFilter_and((f, filterObject)) if scope is None: scope = pureldap.LDAP_SCOPE_wholeSubtree if derefAliases is None: derefAliases = pureldap.LDAP_DEREF_neverDerefAliases if attributes is None: attributes = ["1.1"] results = [] if callback is None: cb = results.append else: cb = callback try: op = pureldap.LDAPSearchRequest( baseObject=self.dn.getText(), scope=scope, derefAliases=derefAliases, sizeLimit=sizeLimit, timeLimit=timeLimit, typesOnly=typesOnly, filter=filterObject, attributes=attributes, ) dsend = self.client.send_multiResponse_ex( op, controls, self._cbSearchMsg, d, cb, complete=not attributes, sizeLimitIsNonFatal=sizeLimitIsNonFatal, ) except ldapclient.LDAPClientConnectionLostException: d.errback(Failure()) else: if callback is None: if return_controls: d.addCallback(lambda ctls: (results, ctls)) else: d.addCallback(lambda dummy: results) def rerouteerr(e): d.errback(e) # returning None will stop the error # from being propagated and logged. dsend.addErrback(rerouteerr) return d
[docs] def lookup(self, dn): e = self.__class__(self.client, dn) d = e.fetch("1.1") return d
# end IConnectedLDAPEntry def __repr__(self): x = {} for key in super().keys(): x[key] = self[key] keys = list(x.keys()) keys.sort() a = [] for key in keys: a.append(f"{repr(key)}: {repr(self[key])}") attributes = ", ".join(a) return "{}(dn={}, attributes={{{}}})".format( self.__class__.__name__, repr(self.dn), attributes )
# API backwards compatibility LDAPEntry = LDAPEntryWithClient
[docs]class LDAPEntryWithAutoFill(LDAPEntry): def __init__(self, *args, **kwargs): LDAPEntry.__init__(self, *args, **kwargs) self.autoFillers = [] def _cb_addAutofiller(self, r, autoFiller): self.autoFillers.append(autoFiller) return r
[docs] def addAutofiller(self, autoFiller): d = defer.maybeDeferred(autoFiller.start, self) d.addCallback(self._cb_addAutofiller, autoFiller) return d
[docs] def journal(self, journalOperation): LDAPEntry.journal(self, journalOperation) for autoFiller in self.autoFillers: autoFiller.notify(self, journalOperation.key)