Package ldaptor :: Package protocols :: Package ldap :: Module ldapsyntax
[hide private]
[frames] | no frames]

Source Code for Module ldaptor.protocols.ldap.ldapsyntax

  1  """Pythonic API for LDAP operations.""" 
  2   
  3  from zope.interface import implements 
  4  from twisted.internet import defer 
  5  from twisted.python.failure import Failure 
  6  from ldaptor.protocols.ldap import ldapclient, ldif, distinguishedname, ldaperrors 
  7  from ldaptor.protocols import pureldap, pureber 
  8  from ldaptor.samba import smbpassword 
  9  from ldaptor import ldapfilter, interfaces, delta, attributeset, entry 
 10  import codecs 
11 -class PasswordSetAggregateError(Exception):
12 """Some of the password plugins failed"""
13 - def __init__(self, errors):
14 Exception.__init__(self) 15 self.errors=errors
16
17 - def __str__(self):
18 return '%s: %s.' % ( 19 self.__doc__, 20 '; '.join([ '%s failed with %s' % (name, fail.getErrorMessage()) 21 for name, fail in self.errors]))
22
23 - def __repr__(self):
24 return '<'+self.__class__.__name__+' errors='+repr(self.errors)+'>'
25
26 -class PasswordSetAborted(Exception):
27 """Aborted""" 28
29 - def __str__(self):
30 return self.__doc__
31
32 -class DNNotPresentError(Exception):
33 """The requested DN cannot be found by the server.""" 34 pass
35
36 -class ObjectInBadStateError(Exception):
37 """The LDAP object in in a bad state.""" 38 pass
39
40 -class ObjectDeletedError(ObjectInBadStateError):
41 """The LDAP object has already been removed, unable to perform operations on it.""" 42 pass
43
44 -class ObjectDirtyError(ObjectInBadStateError):
45 """The LDAP object has a journal which needs to be committed or undone before this operation.""" 46 pass
47
48 -class NoContainingNamingContext(Exception):
49 """The server contains to LDAP naming context that would contain this object.""" 50 pass
51
52 -class CannotRemoveRDNError(Exception):
53 """The attribute to be removed is the RDN for the object and cannot be removed."""
54 - def __init__(self, key, val=None):
55 Exception.__init__(self) 56 self.key=key 57 self.val=val
58
59 - def __str__(self):
60 if self.val is None: 61 r=repr(self.key) 62 else: 63 r='%s=%s' % (repr(self.key), repr(self.val)) 64 return """The attribute to be removed, %s, is the RDN for the object and cannot be removed.""" % r
65
66 -class MatchNotImplemented(NotImplementedError):
67 """Match type not implemented"""
68 - def __init__(self, op):
69 Exception.__init__(self) 70 self.op=op
71
72 - def __str__(self):
73 return '%s: %r' % (self.__doc__, self.op)
74
75 -class JournaledLDAPAttributeSet(attributeset.LDAPAttributeSet):
76 - def __init__(self, ldapObject, *a, **kw):
77 self.ldapObject = ldapObject 78 super(JournaledLDAPAttributeSet, self).__init__(*a, **kw)
79
80 - def add(self, value):
81 self.ldapObject.journal(delta.Add(self.key, [value])) 82 super(JournaledLDAPAttributeSet, self).add(value)
83
84 - def update(self, sequence):
85 self.ldapObject.journal(delta.Add(self.key, sequence)) 86 super(JournaledLDAPAttributeSet, self).update(sequence)
87
88 - def remove(self, value):
89 if value not in self: 90 raise LookupError, value 91 self.ldapObject._canRemove(self.key, value) 92 self.ldapObject.journal(delta.Delete(self.key, [value])) 93 super(JournaledLDAPAttributeSet, self).remove(value)
94
95 - def clear(self):
96 self.ldapObject._canRemoveAll(self.key) 97 super(JournaledLDAPAttributeSet, self).clear() 98 self.ldapObject.journal(delta.Delete(self.key))
99
100 -class LDAPEntryWithClient(entry.EditableLDAPEntry):
101 implements(interfaces.ILDAPEntry, 102 interfaces.IEditableLDAPEntry, 103 interfaces.IConnectedLDAPEntry, 104 ) 105 106 _state = 'invalid' 107 """ 108 109 State of an LDAPEntry is one of: 110 111 invalid - object not initialized yet 112 113 ready - normal 114 115 deleted - object has been deleted 116 117 """ 118
119 - def __init__(self, client, dn, attributes={}, complete=0):
120 """ 121 122 Initialize the object. 123 124 @param client: The LDAP client connection this object belongs 125 to. 126 127 @param dn: Distinguished Name of the object, as a string. 128 129 @param attributes: Attributes of the object. A dictionary of 130 attribute types to list of attribute values. 131 132 """ 133 134 super(LDAPEntryWithClient, self).__init__(dn, attributes) 135 self.client=client 136 self.complete = complete 137 138 self._journal=[] 139 140 self._remoteData = entry.EditableLDAPEntry(dn, attributes) 141 self._state = 'ready'
142
143 - def buildAttributeSet(self, key, values):
144 return JournaledLDAPAttributeSet(self, key, values)
145
146 - def _canRemove(self, key, value):
147 """ 148 149 Called by JournaledLDAPAttributeSet when it is about to remove a value 150 of an attributeType. 151 152 """ 153 self._checkState() 154 for rdn in self.dn.split()[0].split(): 155 if rdn.attributeType == key and rdn.value == value: 156 raise CannotRemoveRDNError, (key, value)
157
158 - def _canRemoveAll(self, key):
159 """ 160 161 Called by JournaledLDAPAttributeSet when it is about to remove all values 162 of an attributeType. 163 164 """ 165 self._checkState() 166 import types 167 assert not isinstance(self.dn, types.StringType) 168 for keyval in self.dn.split()[0].split(): 169 if keyval.attributeType == key: 170 raise CannotRemoveRDNError, (key)
171 172 173
174 - def _checkState(self):
175 if self._state != 'ready': 176 if self._state == 'deleted': 177 raise ObjectDeletedError 178 else: 179 raise ObjectInBadStateError, \ 180 "State is %s while expecting %s" \ 181 % (repr(self._state), repr('ready'))
182
183 - def journal(self, journalOperation):
184 """ 185 186 Add a Modification into the list of modifications 187 that need to be flushed to the LDAP server. 188 189 Normal callers should not use this, they should use the 190 o['foo']=['bar', 'baz'] -style API that enforces schema, 191 handles errors and updates the cached data. 192 193 """ 194 self._journal.append(journalOperation)
195 196 197 # start ILDAPEntry
198 - def __getitem__(self, *a, **kw):
199 self._checkState() 200 return super(LDAPEntryWithClient, self).__getitem__(*a, **kw)
201
202 - def get(self, *a, **kw):
203 self._checkState() 204 return super(LDAPEntryWithClient, self).get(*a, **kw)
205
206 - def has_key(self, *a, **kw):
207 self._checkState() 208 return super(LDAPEntryWithClient, self).has_key(*a, **kw)
209
210 - def __contains__(self, key):
211 self._checkState() 212 return self.has_key(key)
213
214 - def keys(self):
215 self._checkState() 216 return super(LDAPEntryWithClient, self).keys()
217
218 - def items(self):
219 self._checkState() 220 return super(LDAPEntryWithClient, self).items()
221
222 - def __str__(self):
223 a=[] 224 225 objectClasses = list(self.get('objectClass', [])) 226 objectClasses.sort() 227 a.append(('objectClass', objectClasses)) 228 229 l=list(self.items()) 230 l.sort() 231 for key, values in l: 232 if key!='objectClass': 233 a.append((key, values)) 234 return ldif.asLDIF(self.dn, a)
235
236 - def __eq__(self, other):
237 if not isinstance(other, self.__class__): 238 return 0 239 if self.dn != other.dn: 240 return 0 241 242 my=self.keys() 243 my.sort() 244 its=other.keys() 245 its.sort() 246 if my!=its: 247 return 0 248 for key in my: 249 myAttr=self[key] 250 itsAttr=other[key] 251 if myAttr!=itsAttr: 252 return 0 253 return 1
254
255 - def __ne__(self, other):
256 return not self==other
257
258 - def __len__(self):
259 return len(self.keys())
260
261 - def __nonzero__(self):
262 return True
263
264 - def bind(self, password):
265 r=pureldap.LDAPBindRequest(dn=str(self.dn), auth=password) 266 d = self.client.send(r) 267 d.addCallback(self._handle_bind_msg) 268 return d
269
270 - def _handle_bind_msg(self, msg):
271 assert isinstance(msg, pureldap.LDAPBindResponse) 272 assert msg.referral is None #TODO 273 if msg.resultCode!=ldaperrors.Success.resultCode: 274 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 275 return self
276 277 278 # end ILDAPEntry 279 280 # start IEditableLDAPEntry
281 - def __setitem__(self, key, value):
282 self._checkState() 283 self._canRemoveAll(key) 284 285 new=JournaledLDAPAttributeSet(self, key, value) 286 super(LDAPEntryWithClient, self).__setitem__(key, new) 287 self.journal(delta.Replace(key, value))
288
289 - def __delitem__(self, key):
290 self._checkState() 291 self._canRemoveAll(key) 292 293 super(LDAPEntryWithClient, self).__delitem__(key) 294 self.journal(delta.Delete(key))
295
296 - def undo(self):
297 self._checkState() 298 self._attributes.clear() 299 for k, vs in self._remoteData.items(): 300 self._attributes[k] = self.buildAttributeSet(k, vs) 301 self._journal=[]
302
303 - def _commit_success(self, msg):
304 assert isinstance(msg, pureldap.LDAPModifyResponse) 305 assert msg.referral is None #TODO 306 if msg.resultCode!=ldaperrors.Success.resultCode: 307 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 308 309 assert msg.matchedDN=='' 310 311 self._remoteData = entry.EditableLDAPEntry(self.dn, self) 312 self._journal=[] 313 return self
314
315 - def commit(self):
316 self._checkState() 317 if not self._journal: 318 return defer.succeed(self) 319 320 op=pureldap.LDAPModifyRequest( 321 object=str(self.dn), 322 modification=[x.asLDAP() for x in self._journal]) 323 d = defer.maybeDeferred(self.client.send, op) 324 d.addCallback(self._commit_success) 325 return d
326
327 - def _cbMoveDone(self, msg, newDN):
328 assert isinstance(msg, pureldap.LDAPModifyDNResponse) 329 assert msg.referral is None #TODO 330 if msg.resultCode!=ldaperrors.Success.resultCode: 331 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 332 333 assert msg.matchedDN=='' 334 self.dn = newDN 335 return self
336
337 - def move(self, newDN):
338 self._checkState() 339 newDN = distinguishedname.DistinguishedName(newDN) 340 341 newrdn=newDN.split()[0] 342 newSuperior=distinguishedname.DistinguishedName(listOfRDNs=newDN.split()[1:]) 343 newDN = distinguishedname.DistinguishedName((newrdn,) + newSuperior.split()) 344 op = pureldap.LDAPModifyDNRequest(entry=str(self.dn), 345 newrdn=str(newrdn), 346 deleteoldrdn=1, 347 newSuperior=str(newSuperior)) 348 d = self.client.send(op) 349 d.addCallback(self._cbMoveDone, newDN) 350 return d
351
352 - def _cbDeleteDone(self, msg):
353 assert isinstance(msg, pureldap.LDAPResult) 354 if not isinstance(msg, pureldap.LDAPDelResponse): 355 raise ldaperrors.get(msg.resultCode, 356 msg.errorMessage) 357 assert msg.referral is None #TODO 358 if msg.resultCode!=ldaperrors.Success.resultCode: 359 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 360 361 assert msg.matchedDN=='' 362 return self
363
364 - def delete(self):
365 self._checkState() 366 367 op = pureldap.LDAPDelRequest(entry=str(self.dn)) 368 d = self.client.send(op) 369 d.addCallback(self._cbDeleteDone) 370 self._state = 'deleted' 371 return d
372
373 - def _cbAddDone(self, msg, dn):
374 assert isinstance(msg, pureldap.LDAPAddResponse), \ 375 "LDAPRequest response was not an LDAPAddResponse: %r" % msg 376 assert msg.referral is None #TODO 377 if msg.resultCode!=ldaperrors.Success.resultCode: 378 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 379 380 assert msg.matchedDN=='' 381 e = self.__class__(dn=dn, client=self.client) 382 return e
383
384 - def addChild(self, rdn, attributes):
385 self._checkState() 386 387 a = [] 388 if attributes.get('objectClass', None): 389 a.append(('objectClass', attributes['objectClass'])) 390 del attributes['objectClass'] 391 attributes = a+sorted(attributes.items()) 392 del a 393 rdn = distinguishedname.RelativeDistinguishedName(rdn) 394 dn = distinguishedname.DistinguishedName( 395 listOfRDNs=(rdn,)+self.dn.split()) 396 397 ldapAttrs = [] 398 for attrType, values in attributes: 399 ldapAttrType = pureldap.LDAPAttributeDescription(attrType) 400 l = [] 401 for value in values: 402 if (isinstance(value, unicode)): 403 value = value.encode('utf-8') 404 l.append(pureldap.LDAPAttributeValue(value)) 405 ldapValues = pureber.BERSet(l) 406 ldapAttrs.append((ldapAttrType, ldapValues)) 407 op=pureldap.LDAPAddRequest(entry=str(dn), 408 attributes=ldapAttrs) 409 d = self.client.send(op) 410 d.addCallback(self._cbAddDone, dn) 411 return d
412
413 - def _cbSetPassword_ExtendedOperation(self, msg):
414 assert isinstance(msg, pureldap.LDAPExtendedResponse) 415 assert msg.referral is None #TODO 416 if msg.resultCode!=ldaperrors.Success.resultCode: 417 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 418 419 assert msg.matchedDN=='' 420 return self
421
422 - def setPassword_ExtendedOperation(self, newPasswd):
423 """ 424 425 Set the password on this object. 426 427 @param newPasswd: A string containing the new password. 428 429 @return: A Deferred that will complete when the operation is 430 done. 431 432 """ 433 434 self._checkState() 435 436 op = pureldap.LDAPPasswordModifyRequest(userIdentity=str(self.dn), newPasswd=newPasswd) 437 d = self.client.send(op) 438 d.addCallback(self._cbSetPassword_ExtendedOperation) 439 return d
440 441 _setPasswordPriority_ExtendedOperation=0 442 setPasswordMaybe_ExtendedOperation = setPassword_ExtendedOperation 443
444 - def setPassword_Samba(self, newPasswd, style=None):
445 """ 446 447 Set the Samba password on this object. 448 449 @param newPasswd: A string containing the new password. 450 451 @param style: one of 'sambaSamAccount', 'sambaAccount' or 452 None. Specifies the style of samba accounts used. None is 453 default and is the same as 'sambaSamAccount'. 454 455 @return: A Deferred that will complete when the operation is 456 done. 457 458 """ 459 460 self._checkState() 461 462 nthash=smbpassword.nthash(newPasswd) 463 lmhash=smbpassword.lmhash(newPasswd) 464 465 if style is None: 466 style = 'sambaSamAccount' 467 if style == 'sambaSamAccount': 468 self['sambaNTPassword'] = [nthash] 469 self['sambaLMPassword'] = [lmhash] 470 elif style == 'sambaAccount': 471 self['ntPassword'] = [nthash] 472 self['lmPassword'] = [lmhash] 473 else: 474 raise RuntimeError, "Unknown samba password style %r" % style 475 return self.commit()
476 477 _setPasswordPriority_Samba=20
478 - def setPasswordMaybe_Samba(self, newPasswd):
479 """ 480 481 Set the Samba password on this object if it is a 482 sambaSamAccount or sambaAccount. 483 484 @param newPasswd: A string containing the new password. 485 486 @return: A Deferred that will complete when the operation is 487 done. 488 489 """ 490 if not self.complete and not self.has_key('objectClass'): 491 d=self.fetch('objectClass') 492 d.addCallback(lambda dummy, self=self, newPasswd=newPasswd: 493 self.setPasswordMaybe_Samba(newPasswd)) 494 else: 495 objectClasses = [s.upper() for s in self.get('objectClass', ())] 496 if 'sambaAccount'.upper() in objectClasses: 497 d = self.setPassword_Samba(newPasswd, style="sambaAccount") 498 elif 'sambaSamAccount'.upper() in objectClasses: 499 d = self.setPassword_Samba(newPasswd, style="sambaSamAccount") 500 else: 501 d = defer.succeed(self) 502 return d
503
504 - def _cbSetPassword(self, dl, names):
505 assert len(dl)==len(names) 506 l=[] 507 for name, (ok, x) in zip(names, dl): 508 if not ok: 509 l.append((name, x)) 510 if l: 511 raise PasswordSetAggregateError, l 512 return self
513
514 - def _cbSetPassword_one(self, result):
515 return (True, None)
516 - def _ebSetPassword_one(self, fail):
517 fail.trap(ldaperrors.LDAPException, 518 DNNotPresentError) 519 return (False, fail)
520 - def _setPasswordAll(self, results, newPasswd, prefix, names):
521 if not names: 522 return results 523 name, names = names[0], names[1:] 524 if results and not results[-1][0]: 525 # failing 526 fail = Failure(PasswordSetAborted()) 527 d = defer.succeed(results+[(None, fail)]) 528 else: 529 fn = getattr(self, prefix+name) 530 d = defer.maybeDeferred(fn, newPasswd) 531 d.addCallbacks(self._cbSetPassword_one, 532 self._ebSetPassword_one) 533 def cb((success, info)): 534 return results+[(success, info)]
535 d.addCallback(cb) 536 537 d.addCallback(self._setPasswordAll, 538 newPasswd, prefix, names) 539 return d
540
541 - def setPassword(self, newPasswd):
542 def _passwordChangerPriorityComparison(me, other): 543 mePri = getattr(self, '_setPasswordPriority_'+me) 544 otherPri = getattr(self, '_setPasswordPriority_'+other) 545 return cmp(mePri, otherPri)
546 547 prefix='setPasswordMaybe_' 548 names=[name[len(prefix):] for name in dir(self) if name.startswith(prefix)] 549 names.sort(_passwordChangerPriorityComparison) 550 551 d = defer.maybeDeferred(self._setPasswordAll, 552 [], 553 newPasswd, 554 prefix, 555 names) 556 d.addCallback(self._cbSetPassword, names) 557 return d 558 559 # end IEditableLDAPEntry 560 561 # start IConnectedLDAPEntry 562
563 - def _cbNamingContext_Entries(self, results):
564 for result in results: 565 for namingContext in result.get('namingContexts', ()): 566 dn = distinguishedname.DistinguishedName(namingContext) 567 if dn.contains(self.dn): 568 return LDAPEntry(self.client, dn) 569 raise NoContainingNamingContext, self.dn
570
571 - def namingContext(self):
572 o=LDAPEntry(client=self.client, dn='') 573 d=o.search(filterText='(objectClass=*)', 574 scope=pureldap.LDAP_SCOPE_baseObject, 575 attributes=['namingContexts']) 576 d.addCallback(self._cbNamingContext_Entries) 577 return d
578
579 - def _cbFetch(self, results, overWrite):
580 if len(results)!=1: 581 raise DNNotPresentError, self.dn 582 o=results[0] 583 584 assert not self._journal 585 586 if not overWrite: 587 for key in self._remoteData.keys(): 588 del self._remoteData[key] 589 overWrite=o.keys() 590 self.complete = 1 591 592 for k in overWrite: 593 vs=o.get(k) 594 if vs is not None: 595 self._remoteData[k] = vs 596 self.undo() 597 return self
598
599 - def fetch(self, *attributes):
600 self._checkState() 601 if self._journal: 602 raise ObjectDirtyError, 'cannot fetch attributes of %s, it is dirty' % repr(self) 603 604 d = self.search(scope=pureldap.LDAP_SCOPE_baseObject, 605 attributes=attributes) 606 d.addCallback(self._cbFetch, overWrite=attributes) 607 return d
608
609 - def _cbSearchEntry(self, callback, objectName, attributes, complete):
610 attrib={} 611 for key, values in attributes: 612 attrib[str(key)]=[str(x) for x in values] 613 o=LDAPEntry(client=self.client, 614 dn=objectName, 615 attributes=attrib, 616 complete=complete) 617 callback(o)
618
619 - def _cbSearchMsg(self, msg, d, callback, complete, sizeLimitIsNonFatal):
620 if isinstance(msg, pureldap.LDAPSearchResultDone): 621 assert msg.referral is None #TODO 622 e = ldaperrors.get(msg.resultCode, msg.errorMessage) 623 if not isinstance(e, ldaperrors.Success): 624 try: 625 raise e 626 except ldaperrors.LDAPSizeLimitExceeded, e: 627 if sizeLimitIsNonFatal: 628 pass 629 except: 630 d.errback(Failure()) 631 return True 632 633 # search ended successfully 634 assert msg.matchedDN=='' 635 d.callback(None) 636 return True 637 elif isinstance(msg, pureldap.LDAPSearchResultEntry): 638 self._cbSearchEntry(callback, msg.objectName, msg.attributes, 639 complete=complete) 640 return False 641 elif isinstance(msg, pureldap.LDAPSearchResultReference): 642 return False 643 else: 644 raise ldaperrors.LDAPProtocolError, \ 645 'bad search response: %r' % msg
646
647 - def search(self, 648 filterText=None, 649 filterObject=None, 650 attributes=(), 651 scope=None, 652 derefAliases=None, 653 sizeLimit=0, 654 sizeLimitIsNonFatal=False, 655 timeLimit=0, 656 typesOnly=0, 657 callback=None):
658 self._checkState() 659 d=defer.Deferred() 660 if filterObject is None and filterText is None: 661 filterObject=pureldap.LDAPFilterMatchAll 662 elif filterObject is None and filterText is not None: 663 filterObject=ldapfilter.parseFilter(filterText) 664 elif filterObject is not None and filterText is None: 665 pass 666 elif filterObject is not None and filterText is not None: 667 f=ldapfilter.parseFilter(filterText) 668 filterObject=pureldap.LDAPFilter_and((f, filterObject)) 669 670 if scope is None: 671 scope = pureldap.LDAP_SCOPE_wholeSubtree 672 if derefAliases is None: 673 derefAliases = pureldap.LDAP_DEREF_neverDerefAliases 674 675 if attributes is None: 676 attributes = ['1.1'] 677 678 results=[] 679 if callback is None: 680 cb=results.append 681 else: 682 cb=callback 683 try: 684 op = pureldap.LDAPSearchRequest( 685 baseObject=str(self.dn), 686 scope=scope, 687 derefAliases=derefAliases, 688 sizeLimit=sizeLimit, 689 timeLimit=timeLimit, 690 typesOnly=typesOnly, 691 filter=filterObject, 692 attributes=attributes) 693 dsend = self.client.send_multiResponse( 694 op, self._cbSearchMsg, 695 d, cb, complete=not attributes, 696 sizeLimitIsNonFatal=sizeLimitIsNonFatal) 697 except ldapclient.LDAPClientConnectionLostException: 698 d.errback(Failure()) 699 else: 700 if callback is None: 701 d.addCallback(lambda dummy: results) 702 def rerouteerr(e): 703 d.errback(e)
704 # returning None will stop the error 705 # from being propagated and logged. 706 dsend.addErrback(rerouteerr) 707 return d 708
709 - def lookup(self, dn):
710 e = self.__class__(self.client, dn) 711 d = e.fetch('1.1') 712 return d
713 714 # end IConnectedLDAPEntry 715
716 - def __repr__(self):
717 x={} 718 for key in super(LDAPEntryWithClient, self).keys(): 719 x[key]=self[key] 720 keys=x.keys() 721 keys.sort() 722 a=[] 723 for key in keys: 724 a.append('%s: %s' % (repr(key), repr(self[key]))) 725 attributes=', '.join(a) 726 return '%s(dn=%s, attributes={%s})' % ( 727 self.__class__.__name__, 728 repr(str(self.dn)), 729 attributes)
730 731 # API backwards compatibility 732 LDAPEntry = LDAPEntryWithClient 733
734 -class LDAPEntryWithAutoFill(LDAPEntry):
735 - def __init__(self, *args, **kwargs):
736 LDAPEntry.__init__(self, *args, **kwargs) 737 self.autoFillers = []
738
739 - def _cb_addAutofiller(self, r, autoFiller):
740 self.autoFillers.append(autoFiller) 741 return r
742
743 - def addAutofiller(self, autoFiller):
744 d = defer.maybeDeferred(autoFiller.start, self) 745 d.addCallback(self._cb_addAutofiller, autoFiller) 746 return d
747
748 - def journal(self, journalOperation):
749 LDAPEntry.journal(self, journalOperation) 750 for autoFiller in self.autoFillers: 751 autoFiller.notify(self, journalOperation.key)
752